From 1a6a7c1507ad2e1d7c83f59205258396425c16c3 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Thu, 3 Oct 2024 00:13:26 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20product=20review=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/category/dto/CategoryRequest.java | 4 +- .../controller/ProductAdminController.java | 6 +- .../domain/product/entity/Product.java | 11 ++++ .../product/service/ProductAdminService.java | 2 - .../controller/ProductReviewController.java | 23 ++++++++ .../ProductReviewCustomerController.java | 24 ++++++++ .../review/dto/ProductReviewRequest.java | 13 ++++ .../review/dto/ProductReviewResponse.java | 22 +++++++ .../domain/review/entity/ProductReview.java | 59 +++++++++++++++++++ .../review/entity/ProductReviewPhoto.java | 27 +++++++++ .../repository/ProductReviewRepository.java | 13 ++++ .../service/ProductReviewCustomerService.java | 31 ++++++++++ .../review/service/ProductReviewService.java | 34 +++++++++++ 13 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 src/main/java/poomasi/domain/review/controller/ProductReviewController.java create mode 100644 src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java create mode 100644 src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java create mode 100644 src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java create mode 100644 src/main/java/poomasi/domain/review/entity/ProductReview.java create mode 100644 src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java create mode 100644 src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java create mode 100644 src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java create mode 100644 src/main/java/poomasi/domain/review/service/ProductReviewService.java diff --git a/src/main/java/poomasi/domain/category/dto/CategoryRequest.java b/src/main/java/poomasi/domain/category/dto/CategoryRequest.java index 5e6b7eed..761d21c7 100644 --- a/src/main/java/poomasi/domain/category/dto/CategoryRequest.java +++ b/src/main/java/poomasi/domain/category/dto/CategoryRequest.java @@ -2,7 +2,9 @@ import poomasi.domain.category.entity.Category; -public record CategoryRequest(String name) { +public record CategoryRequest( + String name +) { public Category toEntity() { return new Category(name); } diff --git a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java index 3b8a07fe..81c0071f 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java @@ -14,9 +14,5 @@ public class ProductAdminController { private final ProductAdminService productAdminService; - @PutMapping("/api/products/{productId}/open") - ResponseEntity openProduct(@PathVariable Long productId) { - productAdminService.openProduct(productId); - return new ResponseEntity<>(productId, HttpStatus.OK); - } + } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 1e45bd43..1d579a42 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,18 +1,23 @@ package poomasi.domain.product.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.*; import poomasi.domain.product.dto.ProductRegisterRequest; +import poomasi.domain.review.entity.ProductReview; @Entity @Getter @@ -53,6 +58,9 @@ public class Product { @UpdateTimestamp private LocalDateTime updatedAt; + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + List reviewList = new ArrayList<>(); + @Builder public Product(Long productId, @@ -86,4 +94,7 @@ public void addQuantity(Integer quantity) { this.quantity += quantity; } + public void addReview(ProductReview pReview) { + this.reviewList.add(pReview); + } } diff --git a/src/main/java/poomasi/domain/product/service/ProductAdminService.java b/src/main/java/poomasi/domain/product/service/ProductAdminService.java index e55beb8f..1c7a73ee 100644 --- a/src/main/java/poomasi/domain/product/service/ProductAdminService.java +++ b/src/main/java/poomasi/domain/product/service/ProductAdminService.java @@ -17,6 +17,4 @@ private Product getProductByProductId(Long productId) { .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } - public void openProduct(Long productId) { - } } diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewController.java new file mode 100644 index 00000000..b6dee061 --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/ProductReviewController.java @@ -0,0 +1,23 @@ +package poomasi.domain.review.controller; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import poomasi.domain.review.dto.ProductReviewResponse; +import poomasi.domain.review.service.ProductReviewService; + +@Controller +@RequiredArgsConstructor +public class ProductReviewController { + private final ProductReviewService productReviewService; + + @GetMapping("/api/products/{productId}/reviews") + public ResponseEntity getProductReviews(@PathVariable long productId) { + List response = productReviewService.getProductReview(productId); + return new ResponseEntity<>(response, HttpStatus.OK); + } +} diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java new file mode 100644 index 00000000..16ad5bb0 --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java @@ -0,0 +1,24 @@ +package poomasi.domain.review.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import poomasi.domain.review.dto.ProductReviewRequest; +import poomasi.domain.review.service.ProductReviewCustomerService; +import poomasi.domain.review.service.ProductReviewService; + +@Controller +@RequiredArgsConstructor +public class ProductReviewCustomerController { + private final ProductReviewCustomerService productReviewCustomerService; + + @PostMapping("/api/products/{productId}/reviews") + public ResponseEntity registerProductReview(@PathVariable int productId, @RequestBody ProductReviewRequest productReviewRequest){ + long reviewId = productReviewCustomerService.registerReview(productId, productReviewRequest); + return new ResponseEntity<>(reviewId, HttpStatus.CREATED); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java b/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java new file mode 100644 index 00000000..b431940e --- /dev/null +++ b/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java @@ -0,0 +1,13 @@ +package poomasi.domain.review.dto; + +import poomasi.domain.product.entity.Product; +import poomasi.domain.review.entity.ProductReview; + +public record ProductReviewRequest ( + float rating, + String content +){ + public ProductReview toEntity(Product product){ + return new ProductReview(this.rating, this.content, product); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java new file mode 100644 index 00000000..e451cff5 --- /dev/null +++ b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java @@ -0,0 +1,22 @@ +package poomasi.domain.review.dto; + +import poomasi.domain.review.entity.ProductReview; + +public record ProductReviewResponse + (long id , + long productId, + //long reviewerId, + float rating, + String content + ) +{ + public static ProductReviewResponse fromEntity(ProductReview productReview) { + return new ProductReviewResponse( + productReview.getId(), + productReview.getProduct().getId(), + //productReview.getReviewer().getId(), + productReview.getRating(), + productReview.getContent() + ); + } +} diff --git a/src/main/java/poomasi/domain/review/entity/ProductReview.java b/src/main/java/poomasi/domain/review/entity/ProductReview.java new file mode 100644 index 00000000..ab783495 --- /dev/null +++ b/src/main/java/poomasi/domain/review/entity/ProductReview.java @@ -0,0 +1,59 @@ +package poomasi.domain.review.entity; + +import jakarta.persistence.CascadeType; +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.persistence.OneToOne; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +//import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; + +@Entity +@Getter +@NoArgsConstructor +public class ProductReview { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("별점") + private float rating; + + @Comment("리뷰 내용") + private String content; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + private List imageUrl = new ArrayList<>(); + +// @Comment("작성자") +// @ManyToOne +// private Member reviewer; + + public ProductReview(float rating, String content, Product product) { + this.rating = rating; + this.content = content; + this.product = product; + } +} diff --git a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java new file mode 100644 index 00000000..44a22e55 --- /dev/null +++ b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java @@ -0,0 +1,27 @@ +package poomasi.domain.review.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Entity +@NoArgsConstructor +@Getter +public class ProductReviewPhoto { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Comment("리뷰") + @ManyToOne + private ProductReview productReview; + + @Comment("사진 경로") + private String url; + +} diff --git a/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java new file mode 100644 index 00000000..a4319b86 --- /dev/null +++ b/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java @@ -0,0 +1,13 @@ +package poomasi.domain.review.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import poomasi.domain.review.entity.ProductReview; + +@Repository +public interface ProductReviewRepository extends JpaRepository { + @Query("select r from ProductReview r where r.product.id = :productId") + List findByProductId(long productId); +} diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java new file mode 100644 index 00000000..b447ce6a --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java @@ -0,0 +1,31 @@ +package poomasi.domain.review.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.review.dto.ProductReviewRequest; +import poomasi.domain.review.entity.ProductReview; +import poomasi.domain.review.repository.ProductReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ProductReviewCustomerService { + private final ProductReviewRepository productReviewRepository; + private final ProductRepository productRepository; + + public long registerReview(long productId, ProductReviewRequest productReviewRequest) { + //이미지 저장하고 주소 받아와서 review에 추가해주기 + Product product = getProductByProductId(productId); + ProductReview pReview = productReviewRequest.toEntity(product); + pReview = productReviewRepository.save(pReview); + product.addReview(pReview); + return pReview.getId(); + } + private Product getProductByProductId(long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + } +} diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewService.java b/src/main/java/poomasi/domain/review/service/ProductReviewService.java new file mode 100644 index 00000000..803366a4 --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/ProductReviewService.java @@ -0,0 +1,34 @@ +package poomasi.domain.review.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.review.dto.ProductReviewRequest; +import poomasi.domain.review.dto.ProductReviewResponse; +import poomasi.domain.review.entity.ProductReview; +import poomasi.domain.review.repository.ProductReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ProductReviewService { + private final ProductReviewRepository productReviewRepository; + private final ProductRepository productRepository; + + public List getProductReview(long productId) { + getProductByProductId(productId); //상품이 존재하는지 체크 + + return productReviewRepository.findByProductId(productId).stream() + .map(ProductReviewResponse::fromEntity).toList(); + } + + private Product getProductByProductId(long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + } + + +} From 18db3cbe7ac80e03f53b6e1087dfcc606af98d87 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Thu, 3 Oct 2024 16:53:41 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20category=20-=20product=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/category/entity/Category.java | 16 ++++++++++++ .../product/dto/ProductRegisterRequest.java | 7 ++--- .../domain/product/entity/Product.java | 22 +++++++++------- .../product/service/ProductFarmerService.java | 26 ++++++++++++++++--- .../ProductReviewCustomerController.java | 8 ++++++ .../service/ProductReviewCustomerService.java | 14 ++++++++++ .../poomasi/global/error/BusinessError.java | 4 +-- 7 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/main/java/poomasi/domain/category/entity/Category.java b/src/main/java/poomasi/domain/category/entity/Category.java index 79b9a251..93b4963b 100644 --- a/src/main/java/poomasi/domain/category/entity/Category.java +++ b/src/main/java/poomasi/domain/category/entity/Category.java @@ -1,12 +1,17 @@ package poomasi.domain.category.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; import poomasi.domain.category.dto.CategoryRequest; +import poomasi.domain.product.entity.Product; @Entity @Getter @@ -17,6 +22,9 @@ public class Category { private long id; private String name; + @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true) + List products = new ArrayList<>(); + public Category(String name) { this.name = name; } @@ -24,4 +32,12 @@ public Category(String name) { public void modifyName(CategoryRequest categoryRequest) { this.name = categoryRequest.name(); } + + public void deleteProduct(Product product) { + this.products.remove(product); + } + + public void addProduct(Product saveProduct) { + this.products.add(saveProduct); + } } diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java index 2bf8689f..cb9c76b9 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java @@ -1,5 +1,6 @@ package poomasi.domain.product.dto; +import poomasi.domain.category.entity.Category; import poomasi.domain.product.entity.Product; public record ProductRegisterRequest( @@ -9,12 +10,12 @@ public record ProductRegisterRequest( String description, String imageUrl, int quantity, - String price + int price ) { - public Product toEntity() { + public Product toEntity(Category category) { return Product.builder() - .categoryId(categoryId) + .category(category) .farmerId(farmerId) .name(name) .description(description) diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 1d579a42..1af0627f 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -2,10 +2,12 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import java.time.LocalDateTime; @@ -16,6 +18,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.*; +import poomasi.domain.category.entity.Category; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.ProductReview; @@ -29,7 +32,8 @@ public class Product { private Long id; @Comment("카테고리 ID") - private Long categoryId; + @ManyToOne(fetch = FetchType.LAZY) + private Category category; @Comment("등록한 사람") private Long farmerId; //등록한 사람 @@ -47,7 +51,7 @@ public class Product { private int quantity; @Comment("가격") - private String price; + private int price; @Comment("삭제 일시") private LocalDateTime deletedAt; @@ -58,20 +62,19 @@ public class Product { @UpdateTimestamp private LocalDateTime updatedAt; - @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + @OneToMany(mappedBy = "product_review", cascade = CascadeType.REMOVE, orphanRemoval = true) List reviewList = new ArrayList<>(); - @Builder public Product(Long productId, - Long categoryId, + Category category, Long farmerId, //등록한 사람 String name, String description, String imageUrl, int quantity, - String price) { - this.categoryId = categoryId; + int price) { + this.category = category; this.farmerId = farmerId; this.name = name; this.description = description; @@ -80,8 +83,8 @@ public Product(Long productId, this.price = price; } - public Product modify(ProductRegisterRequest productRegisterRequest) { - this.categoryId = productRegisterRequest.categoryId(); + public Product modify(Category category, ProductRegisterRequest productRegisterRequest) { + this.category = category; this.name = productRegisterRequest.name(); this.description = productRegisterRequest.description(); this.imageUrl = productRegisterRequest.imageUrl(); @@ -97,4 +100,5 @@ public void addQuantity(Integer quantity) { public void addReview(ProductReview pReview) { this.reviewList.add(pReview); } + } diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index af3b5166..4347e719 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -3,6 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.category.entity.Category; +import poomasi.domain.category.repository.CategoryRepository; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; @@ -14,24 +16,40 @@ public class ProductFarmerService { private final ProductRepository productRepository; - - public Long registerProduct(ProductRegisterRequest product) { + private final CategoryRepository categoryRepository; + public Long registerProduct(ProductRegisterRequest productRequest) { //token이 farmer인지 확인하기 + Category category = getCategory(productRequest); + Product saveProduct = productRepository.save(productRequest.toEntity(category)); + category.addProduct(saveProduct); - Product saveProduct = productRepository.save(product.toEntity()); + System.out.println(category.getProducts().size()); return saveProduct.getId(); } + private Category getCategory(ProductRegisterRequest product) { + return categoryRepository.findById(product.categoryId()) + .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + } + + @Transactional public void modifyProduct(ProductRegisterRequest productRequest, Long productId) { //주인인지 알아보기 + Category category = getCategory(productRequest); Product product = getProductByProductId(productId); - productRepository.save(product.modify(productRequest)); + + product.getCategory().deleteProduct(product); //원래 카테고리에서 상품 삭제 + product = productRepository.save(product.modify(category, productRequest)); //상품 갱신 + category.addProduct(product);//새로운 카테고리에 추가 } @Transactional public void deleteProduct(Long productId) { //주인인지 알아보기 Product product = getProductByProductId(productId); + Category category = product.getCategory(); + + category.deleteProduct(product); productRepository.delete(product); } diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java index 16ad5bb0..034c89b6 100644 --- a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java +++ b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java @@ -1,9 +1,11 @@ package poomasi.domain.review.controller; import lombok.RequiredArgsConstructor; +import org.apache.coyote.Response; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,4 +23,10 @@ public ResponseEntity registerProductReview(@PathVariable int productId, @Req long reviewId = productReviewCustomerService.registerReview(productId, productReviewRequest); return new ResponseEntity<>(reviewId, HttpStatus.CREATED); } + + @DeleteMapping("/api/reviews/{reviewId}") + public ResponseEntity deleteProductReview(@PathVariable long reviewId){ + productReviewCustomerService.deleteReview(reviewId); + return new ResponseEntity<>(HttpStatus.OK); + } } diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java index b447ce6a..ebad9f56 100644 --- a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java +++ b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java @@ -1,7 +1,9 @@ package poomasi.domain.review.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; import poomasi.domain.review.dto.ProductReviewRequest; @@ -24,8 +26,20 @@ public long registerReview(long productId, ProductReviewRequest productReviewReq product.addReview(pReview); return pReview.getId(); } + + private Product getProductByProductId(long productId) { return productRepository.findById(productId) .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } + + @Transactional + public void deleteReview(long reviewId) { + ProductReview review = getReviewById(reviewId); + productReviewRepository.delete(review); + } + + private ProductReview getReviewById(long reviewId) { + return productReviewRepository.findById(reviewId).orElseThrow(()-> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); + } } diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 240b2bb4..a83f82ee 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -11,10 +11,10 @@ public enum BusinessError { // Product PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), - // Category CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다."), - + // Review + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "중복된 이메일입니다."), From 1e7b42f19326c868b86d0dd9305f261812c32a42 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Thu, 3 Oct 2024 16:54:24 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=82=B4=20=EC=83=81=ED=92=88=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../category/controller/CategoryController.java | 8 ++++++++ .../domain/category/service/CategoryService.java | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/poomasi/domain/category/controller/CategoryController.java b/src/main/java/poomasi/domain/category/controller/CategoryController.java index a15c9118..5aa9568b 100644 --- a/src/main/java/poomasi/domain/category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/category/controller/CategoryController.java @@ -4,8 +4,10 @@ import org.springframework.http.HttpStatus; 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.RestController; import poomasi.domain.category.dto.CategoryResponse; +import poomasi.domain.category.dto.ProductListInCategoryResponse; import poomasi.domain.category.service.CategoryService; import java.util.List; @@ -20,4 +22,10 @@ public ResponseEntity getAllCategories() { List categories = categoryService.getAllCategories(); return new ResponseEntity<>(categories, HttpStatus.OK); } + + @GetMapping("/api/categories/{categoryId}") + public ResponseEntity getCategoryById(@PathVariable long categoryId) { + List productList = categoryService.getProductInCategory(categoryId); + return new ResponseEntity<>(productList, HttpStatus.OK); + } } diff --git a/src/main/java/poomasi/domain/category/service/CategoryService.java b/src/main/java/poomasi/domain/category/service/CategoryService.java index b1ff168f..1ca38277 100644 --- a/src/main/java/poomasi/domain/category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/category/service/CategoryService.java @@ -4,13 +4,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.category.dto.CategoryResponse; +import poomasi.domain.category.dto.ProductListInCategoryResponse; import poomasi.domain.category.entity.Category; import poomasi.domain.category.repository.CategoryRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; @Service @RequiredArgsConstructor public class CategoryService { - private final CategoryRepository categoryRepository; public List getAllCategories() { @@ -19,4 +21,15 @@ public List getAllCategories() { .map(CategoryResponse::fromEntity) .toList(); } + + public List getProductInCategory(long categoryId) { + Category category = getCategory(categoryId); + return category.getProducts().stream() + .map(ProductListInCategoryResponse::fromEntity).toList(); + } + + private Category getCategory(long categoryId){ + return categoryRepository.findById(categoryId) + .orElseThrow(()->new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + } } From 74a4881cdf2f4748a1810458238937725630a123 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Thu, 3 Oct 2024 16:56:16 +0900 Subject: [PATCH 04/17] =?UTF-8?q?fix:=20=EB=B9=A0=EC=A7=84=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/ProductListInCategoryResponse.java | 24 ++++++++++++++ ...Controller.java => ProductController.java} | 9 ++++-- .../domain/product/dto/ProductResponse.java | 31 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java rename src/main/java/poomasi/domain/product/controller/{ProductAdminController.java => ProductController.java} (65%) create mode 100644 src/main/java/poomasi/domain/product/dto/ProductResponse.java diff --git a/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java b/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java new file mode 100644 index 00000000..e0a4e2b7 --- /dev/null +++ b/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java @@ -0,0 +1,24 @@ +package poomasi.domain.category.dto; + +import poomasi.domain.product.entity.Product; + +public record ProductListInCategoryResponse( + Long categoryId, + //Long farmerId, //등록한 사람 + String name, + String description, + String imageUrl, + int quantity, + int price +) { + public static ProductListInCategoryResponse fromEntity(Product product) { + return new ProductListInCategoryResponse( + product.getCategory().getId(), + product.getName(), + product.getDescription(), + product.getImageUrl(), + product.getQuantity(), + product.getPrice() + ); + } +} diff --git a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java b/src/main/java/poomasi/domain/product/controller/ProductController.java similarity index 65% rename from src/main/java/poomasi/domain/product/controller/ProductAdminController.java rename to src/main/java/poomasi/domain/product/controller/ProductController.java index 81c0071f..653367bb 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductController.java @@ -1,18 +1,21 @@ package poomasi.domain.product.controller; 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.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RestController; import poomasi.domain.product.service.ProductAdminService; @RestController @RequiredArgsConstructor -public class ProductAdminController { +public class ProductController { private final ProductAdminService productAdminService; +// @GetMapping("/api/products/{productId") +// public ResponseEntity getProducts(@PathVariable int productId) { +// +// } } diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java new file mode 100644 index 00000000..95ca16d8 --- /dev/null +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -0,0 +1,31 @@ +package poomasi.domain.product.dto; + +import java.util.List; +import poomasi.domain.product.entity.Product; +import poomasi.domain.review.entity.ProductReview; + +public record ProductResponse ( + long id, + long categoryId, + long farmerId, + String name, + String description, + String imageUrl, + int quantity, + int price, + List reviewList +){ + public ProductResponse fromEntity(Product product) { + return new ProductResponse( + product.getId(), + product.getCategory().getId(), + product.getFarmerId(), + product.getName(), + product.getDescription(), + product.getImageUrl(), + product.getQuantity(), + product.getPrice(), + product.getReviewList() + ); + } +} From bbd18af6f3c29230510bf067d00ec4524dddef09 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Thu, 3 Oct 2024 17:11:00 +0900 Subject: [PATCH 05/17] =?UTF-8?q?style:=20=EC=95=84=20=EB=A7=9E=EB=8B=A4?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/poomasi/Application.java | 1 + .../controller/CategoryAdminController.java | 7 +++++- .../controller/CategoryController.java | 5 ++-- .../domain/category/dto/CategoryRequest.java | 1 + .../domain/category/dto/CategoryResponse.java | 1 + .../dto/ProductListInCategoryResponse.java | 1 + .../domain/category/entity/Category.java | 1 + .../service/CategoryAdminService.java | 2 +- .../category/service/CategoryService.java | 5 ++-- .../farm/controller/FarmController.java | 1 + .../farm/controller/FarmFarmerController.java | 1 + .../domain/farm/dto/FarmRegisterRequest.java | 1 + .../poomasi/domain/farm/dto/FarmResponse.java | 3 ++- .../domain/farm/dto/FarmUpdateRequest.java | 1 + .../java/poomasi/domain/farm/entity/Farm.java | 14 +++++++---- .../farm/repository/FarmRepository.java | 1 + .../farm/service/FarmFarmerService.java | 11 +++++---- .../domain/farm/service/FarmService.java | 7 +++--- .../product/controller/ProductController.java | 3 --- .../controller/ProductFarmerController.java | 5 ++-- .../domain/product/dto/ProductResponse.java | 5 ++-- .../domain/product/entity/Product.java | 23 ++++++++++--------- .../product/service/ProductAdminService.java | 1 + .../product/service/ProductFarmerService.java | 3 ++- .../controller/ProductReviewController.java | 1 + .../ProductReviewCustomerController.java | 11 +++++---- .../review/dto/ProductReviewRequest.java | 7 +++--- .../review/dto/ProductReviewResponse.java | 6 ++--- .../domain/review/entity/ProductReview.java | 4 +--- .../review/entity/ProductReviewPhoto.java | 1 + .../repository/ProductReviewRepository.java | 1 + .../service/ProductReviewCustomerService.java | 5 ++-- .../review/service/ProductReviewService.java | 3 +-- .../global/error/ApplicationException.java | 1 + .../global/error/BusinessException.java | 1 + 35 files changed, 87 insertions(+), 58 deletions(-) diff --git a/src/main/java/poomasi/Application.java b/src/main/java/poomasi/Application.java index a8175566..d8253f19 100644 --- a/src/main/java/poomasi/Application.java +++ b/src/main/java/poomasi/Application.java @@ -8,6 +8,7 @@ @EnableJpaAuditing @SpringBootApplication(exclude = SecurityAutoConfiguration.class) public class Application { + public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/src/main/java/poomasi/domain/category/controller/CategoryAdminController.java b/src/main/java/poomasi/domain/category/controller/CategoryAdminController.java index 48ac2c56..6f6ce625 100644 --- a/src/main/java/poomasi/domain/category/controller/CategoryAdminController.java +++ b/src/main/java/poomasi/domain/category/controller/CategoryAdminController.java @@ -3,7 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; import poomasi.domain.category.dto.CategoryRequest; import poomasi.domain.category.service.CategoryAdminService; diff --git a/src/main/java/poomasi/domain/category/controller/CategoryController.java b/src/main/java/poomasi/domain/category/controller/CategoryController.java index 5aa9568b..79fe0951 100644 --- a/src/main/java/poomasi/domain/category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/category/controller/CategoryController.java @@ -1,5 +1,6 @@ package poomasi.domain.category.controller; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,7 +11,6 @@ import poomasi.domain.category.dto.ProductListInCategoryResponse; import poomasi.domain.category.service.CategoryService; -import java.util.List; @RestController @RequiredArgsConstructor public class CategoryController { @@ -25,7 +25,8 @@ public ResponseEntity getAllCategories() { @GetMapping("/api/categories/{categoryId}") public ResponseEntity getCategoryById(@PathVariable long categoryId) { - List productList = categoryService.getProductInCategory(categoryId); + List productList = categoryService.getProductInCategory( + categoryId); return new ResponseEntity<>(productList, HttpStatus.OK); } } diff --git a/src/main/java/poomasi/domain/category/dto/CategoryRequest.java b/src/main/java/poomasi/domain/category/dto/CategoryRequest.java index 761d21c7..8e8740c8 100644 --- a/src/main/java/poomasi/domain/category/dto/CategoryRequest.java +++ b/src/main/java/poomasi/domain/category/dto/CategoryRequest.java @@ -5,6 +5,7 @@ public record CategoryRequest( String name ) { + public Category toEntity() { return new Category(name); } diff --git a/src/main/java/poomasi/domain/category/dto/CategoryResponse.java b/src/main/java/poomasi/domain/category/dto/CategoryResponse.java index c8cfb5bb..15b400a4 100644 --- a/src/main/java/poomasi/domain/category/dto/CategoryResponse.java +++ b/src/main/java/poomasi/domain/category/dto/CategoryResponse.java @@ -3,6 +3,7 @@ import poomasi.domain.category.entity.Category; public record CategoryResponse(long id, String name) { + public static CategoryResponse fromEntity(Category category) { return new CategoryResponse(category.getId(), category.getName()); } diff --git a/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java b/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java index e0a4e2b7..47f49eb8 100644 --- a/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java +++ b/src/main/java/poomasi/domain/category/dto/ProductListInCategoryResponse.java @@ -11,6 +11,7 @@ public record ProductListInCategoryResponse( int quantity, int price ) { + public static ProductListInCategoryResponse fromEntity(Product product) { return new ProductListInCategoryResponse( product.getCategory().getId(), diff --git a/src/main/java/poomasi/domain/category/entity/Category.java b/src/main/java/poomasi/domain/category/entity/Category.java index 93b4963b..ad4ffcd9 100644 --- a/src/main/java/poomasi/domain/category/entity/Category.java +++ b/src/main/java/poomasi/domain/category/entity/Category.java @@ -17,6 +17,7 @@ @Getter @NoArgsConstructor public class Category { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; diff --git a/src/main/java/poomasi/domain/category/service/CategoryAdminService.java b/src/main/java/poomasi/domain/category/service/CategoryAdminService.java index a64212cf..720faa96 100644 --- a/src/main/java/poomasi/domain/category/service/CategoryAdminService.java +++ b/src/main/java/poomasi/domain/category/service/CategoryAdminService.java @@ -1,6 +1,5 @@ package poomasi.domain.category.service; -import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,6 +12,7 @@ @Service @RequiredArgsConstructor public class CategoryAdminService { + private final CategoryRepository categoryRepository; public Long registerCategory(CategoryRequest categoryRequest) { diff --git a/src/main/java/poomasi/domain/category/service/CategoryService.java b/src/main/java/poomasi/domain/category/service/CategoryService.java index 1ca38277..39c1f420 100644 --- a/src/main/java/poomasi/domain/category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/category/service/CategoryService.java @@ -13,6 +13,7 @@ @Service @RequiredArgsConstructor public class CategoryService { + private final CategoryRepository categoryRepository; public List getAllCategories() { @@ -28,8 +29,8 @@ public List getProductInCategory(long categoryId) .map(ProductListInCategoryResponse::fromEntity).toList(); } - private Category getCategory(long categoryId){ + private Category getCategory(long categoryId) { return categoryRepository.findById(categoryId) - .orElseThrow(()->new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); } } diff --git a/src/main/java/poomasi/domain/farm/controller/FarmController.java b/src/main/java/poomasi/domain/farm/controller/FarmController.java index 9c7c1b27..6a658b04 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmController.java @@ -14,6 +14,7 @@ @RequiredArgsConstructor @RequestMapping("/api/farm") public class FarmController { + private final FarmService farmService; @GetMapping("/{farmId}") diff --git a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java index 71a3aa33..68f84b33 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java @@ -15,6 +15,7 @@ @AllArgsConstructor @RequestMapping("/api/farm") public class FarmFarmerController { + private final FarmFarmerService farmFarmerService; // TODO: 판매자만 접근가능하도록 인증/인가 annotation 추가 diff --git a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java index 2f60d40a..dd2a2bed 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java @@ -12,6 +12,7 @@ public record FarmRegisterRequest( String phoneNumber, String description ) { + public Farm toEntity() { return Farm.builder() .name(name) diff --git a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java index 74021c34..313f57be 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java @@ -11,7 +11,8 @@ public record FarmResponse( // FIXME: 사용자 정보 추가 및 설명/전화 Double latitude, Double longitude, String description - ) { +) { + public static FarmResponse fromEntity(Farm farm) { return new FarmResponse( farm.getId(), diff --git a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java index 23e5ebbe..fb746802 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java @@ -12,6 +12,7 @@ public record FarmUpdateRequest( Double latitude, Double longitude ) { + public Farm toEntity(Farm farm) { return farm.updateFarm(this); } diff --git a/src/main/java/poomasi/domain/farm/entity/Farm.java b/src/main/java/poomasi/domain/farm/entity/Farm.java index e89db2bf..c6fe3c4d 100644 --- a/src/main/java/poomasi/domain/farm/entity/Farm.java +++ b/src/main/java/poomasi/domain/farm/entity/Farm.java @@ -1,6 +1,12 @@ package poomasi.domain.farm.entity; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -11,8 +17,6 @@ import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.farm.dto.FarmUpdateRequest; -import java.time.LocalDateTime; - @Entity @Getter @Table(name = "farm") @@ -20,6 +24,7 @@ @SQLDelete(sql = "UPDATE farm SET deleted = true WHERE id = ?") @SQLSelect(sql = "SELECT * FROM farm WHERE deleted = false") public class Farm { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -51,7 +56,8 @@ public class Farm { private LocalDateTime updatedAt = LocalDateTime.now(); @Builder - public Farm(String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description) { + public Farm(String name, Long ownerId, String address, String addressDetail, Double latitude, + Double longitude, String description) { this.name = name; this.ownerId = ownerId; this.address = address; diff --git a/src/main/java/poomasi/domain/farm/repository/FarmRepository.java b/src/main/java/poomasi/domain/farm/repository/FarmRepository.java index 7c4c6e9b..4ea13cb4 100644 --- a/src/main/java/poomasi/domain/farm/repository/FarmRepository.java +++ b/src/main/java/poomasi/domain/farm/repository/FarmRepository.java @@ -8,5 +8,6 @@ @Repository public interface FarmRepository extends JpaRepository { + Page findAll(Pageable pageable); } diff --git a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java index 7a967eb8..553d38b6 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java @@ -1,6 +1,8 @@ package poomasi.domain.farm.service; -import lombok.AllArgsConstructor; +import static poomasi.global.error.BusinessError.FARM_NOT_FOUND; +import static poomasi.global.error.BusinessError.FARM_OWNER_MISMATCH; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.farm.dto.FarmRegisterRequest; @@ -9,12 +11,10 @@ import poomasi.domain.farm.repository.FarmRepository; import poomasi.global.error.BusinessException; -import static poomasi.global.error.BusinessError.FARM_NOT_FOUND; -import static poomasi.global.error.BusinessError.FARM_OWNER_MISMATCH; - @Service @RequiredArgsConstructor public class FarmFarmerService { + private final FarmRepository farmRepository; public Long registerFarm(FarmRegisterRequest request) { @@ -38,7 +38,8 @@ public Long updateFarm(Long farmerId, FarmUpdateRequest request) { } public Farm getFarmByFarmId(Long farmId) { - return farmRepository.findById(farmId).orElseThrow(() -> new BusinessException(FARM_NOT_FOUND)); + return farmRepository.findById(farmId) + .orElseThrow(() -> new BusinessException(FARM_NOT_FOUND)); } } diff --git a/src/main/java/poomasi/domain/farm/service/FarmService.java b/src/main/java/poomasi/domain/farm/service/FarmService.java index 99e1bc1d..9584a681 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmService.java @@ -1,6 +1,7 @@ package poomasi.domain.farm.service; -import lombok.AllArgsConstructor; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -9,12 +10,10 @@ import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class FarmService { + private final FarmRepository farmRepository; public FarmResponse getFarmByFarmId(Long farmId) { diff --git a/src/main/java/poomasi/domain/product/controller/ProductController.java b/src/main/java/poomasi/domain/product/controller/ProductController.java index 653367bb..e0f57848 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductController.java @@ -1,9 +1,6 @@ package poomasi.domain.product.controller; import lombok.RequiredArgsConstructor; -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.RestController; import poomasi.domain.product.service.ProductAdminService; diff --git a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java index cd551f47..1b69933a 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java @@ -1,6 +1,5 @@ package poomasi.domain.product.controller; -import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -28,7 +27,7 @@ public ResponseEntity registerProduct(@RequestBody ProductRegisterRequest pro @PutMapping("/api/products/{productId}") public ResponseEntity modifyProduct(@RequestBody ProductRegisterRequest product, - @PathVariable Long productId) { + @PathVariable Long productId) { productFarmerService.modifyProduct(product, productId); return new ResponseEntity<>(productId, HttpStatus.OK); } @@ -41,7 +40,7 @@ public ResponseEntity deleteProduct(@PathVariable Long productId) { @PatchMapping("/api/products/{productId}/count/{quantity}") public ResponseEntity addQuantity(@PathVariable Long productId, - @PathVariable Integer quantity) { + @PathVariable Integer quantity) { productFarmerService.addQuantity(productId, quantity); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 95ca16d8..c49bb611 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -4,7 +4,7 @@ import poomasi.domain.product.entity.Product; import poomasi.domain.review.entity.ProductReview; -public record ProductResponse ( +public record ProductResponse( long id, long categoryId, long farmerId, @@ -14,7 +14,8 @@ public record ProductResponse ( int quantity, int price, List reviewList -){ +) { + public ProductResponse fromEntity(Product product) { return new ProductResponse( product.getId(), diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 1af0627f..fa3ece04 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -6,18 +6,18 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; - import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import java.time.LocalDateTime; - import java.util.ArrayList; import java.util.List; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.*; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.category.entity.Category; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.ProductReview; @@ -27,6 +27,7 @@ @NoArgsConstructor @SQLDelete(sql = "UPDATE product SET deletedAt = current_timestamp WHERE id = ?") public class Product { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -67,13 +68,13 @@ public class Product { @Builder public Product(Long productId, - Category category, - Long farmerId, //등록한 사람 - String name, - String description, - String imageUrl, - int quantity, - int price) { + Category category, + Long farmerId, //등록한 사람 + String name, + String description, + String imageUrl, + int quantity, + int price) { this.category = category; this.farmerId = farmerId; this.name = name; diff --git a/src/main/java/poomasi/domain/product/service/ProductAdminService.java b/src/main/java/poomasi/domain/product/service/ProductAdminService.java index 1c7a73ee..86a59f5a 100644 --- a/src/main/java/poomasi/domain/product/service/ProductAdminService.java +++ b/src/main/java/poomasi/domain/product/service/ProductAdminService.java @@ -10,6 +10,7 @@ @Service @RequiredArgsConstructor public class ProductAdminService { + private final ProductRepository productRepository; private Product getProductByProductId(Long productId) { diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index 4347e719..e9b08fa9 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -16,7 +16,8 @@ public class ProductFarmerService { private final ProductRepository productRepository; - private final CategoryRepository categoryRepository; + private final CategoryRepository categoryRepository; + public Long registerProduct(ProductRegisterRequest productRequest) { //token이 farmer인지 확인하기 Category category = getCategory(productRequest); diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewController.java index b6dee061..b7387e2c 100644 --- a/src/main/java/poomasi/domain/review/controller/ProductReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/ProductReviewController.java @@ -13,6 +13,7 @@ @Controller @RequiredArgsConstructor public class ProductReviewController { + private final ProductReviewService productReviewService; @GetMapping("/api/products/{productId}/reviews") diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java index 034c89b6..c009943f 100644 --- a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java +++ b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java @@ -1,7 +1,6 @@ package poomasi.domain.review.controller; import lombok.RequiredArgsConstructor; -import org.apache.coyote.Response; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -11,21 +10,23 @@ import org.springframework.web.bind.annotation.RequestBody; import poomasi.domain.review.dto.ProductReviewRequest; import poomasi.domain.review.service.ProductReviewCustomerService; -import poomasi.domain.review.service.ProductReviewService; @Controller @RequiredArgsConstructor public class ProductReviewCustomerController { + private final ProductReviewCustomerService productReviewCustomerService; @PostMapping("/api/products/{productId}/reviews") - public ResponseEntity registerProductReview(@PathVariable int productId, @RequestBody ProductReviewRequest productReviewRequest){ - long reviewId = productReviewCustomerService.registerReview(productId, productReviewRequest); + public ResponseEntity registerProductReview(@PathVariable int productId, + @RequestBody ProductReviewRequest productReviewRequest) { + long reviewId = productReviewCustomerService.registerReview(productId, + productReviewRequest); return new ResponseEntity<>(reviewId, HttpStatus.CREATED); } @DeleteMapping("/api/reviews/{reviewId}") - public ResponseEntity deleteProductReview(@PathVariable long reviewId){ + public ResponseEntity deleteProductReview(@PathVariable long reviewId) { productReviewCustomerService.deleteReview(reviewId); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java b/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java index b431940e..b068c888 100644 --- a/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java +++ b/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java @@ -3,11 +3,12 @@ import poomasi.domain.product.entity.Product; import poomasi.domain.review.entity.ProductReview; -public record ProductReviewRequest ( +public record ProductReviewRequest( float rating, String content -){ - public ProductReview toEntity(Product product){ +) { + + public ProductReview toEntity(Product product) { return new ProductReview(this.rating, this.content, product); } } diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java index e451cff5..255c1947 100644 --- a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java +++ b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java @@ -3,13 +3,13 @@ import poomasi.domain.review.entity.ProductReview; public record ProductReviewResponse - (long id , + (long id, long productId, //long reviewerId, float rating, String content - ) -{ + ) { + public static ProductReviewResponse fromEntity(ProductReview productReview) { return new ProductReviewResponse( productReview.getId(), diff --git a/src/main/java/poomasi/domain/review/entity/ProductReview.java b/src/main/java/poomasi/domain/review/entity/ProductReview.java index ab783495..6014dc1a 100644 --- a/src/main/java/poomasi/domain/review/entity/ProductReview.java +++ b/src/main/java/poomasi/domain/review/entity/ProductReview.java @@ -6,10 +6,8 @@ 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.persistence.OneToOne; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -18,13 +16,13 @@ import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; -//import poomasi.domain.member.entity.Member; import poomasi.domain.product.entity.Product; @Entity @Getter @NoArgsConstructor public class ProductReview { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java index 44a22e55..7d692b68 100644 --- a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java +++ b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java @@ -13,6 +13,7 @@ @NoArgsConstructor @Getter public class ProductReviewPhoto { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; diff --git a/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java index a4319b86..4b448d15 100644 --- a/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java +++ b/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java @@ -8,6 +8,7 @@ @Repository public interface ProductReviewRepository extends JpaRepository { + @Query("select r from ProductReview r where r.product.id = :productId") List findByProductId(long productId); } diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java index ebad9f56..e487fcd5 100644 --- a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java +++ b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java @@ -1,7 +1,6 @@ package poomasi.domain.review.service; import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.Modifying; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import poomasi.domain.product.entity.Product; @@ -15,6 +14,7 @@ @Service @RequiredArgsConstructor public class ProductReviewCustomerService { + private final ProductReviewRepository productReviewRepository; private final ProductRepository productRepository; @@ -40,6 +40,7 @@ public void deleteReview(long reviewId) { } private ProductReview getReviewById(long reviewId) { - return productReviewRepository.findById(reviewId).orElseThrow(()-> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); + return productReviewRepository.findById(reviewId) + .orElseThrow(() -> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); } } diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewService.java b/src/main/java/poomasi/domain/review/service/ProductReviewService.java index 803366a4..f2792bb7 100644 --- a/src/main/java/poomasi/domain/review/service/ProductReviewService.java +++ b/src/main/java/poomasi/domain/review/service/ProductReviewService.java @@ -5,9 +5,7 @@ import org.springframework.stereotype.Service; import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; -import poomasi.domain.review.dto.ProductReviewRequest; import poomasi.domain.review.dto.ProductReviewResponse; -import poomasi.domain.review.entity.ProductReview; import poomasi.domain.review.repository.ProductReviewRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -15,6 +13,7 @@ @Service @RequiredArgsConstructor public class ProductReviewService { + private final ProductReviewRepository productReviewRepository; private final ProductRepository productRepository; diff --git a/src/main/java/poomasi/global/error/ApplicationException.java b/src/main/java/poomasi/global/error/ApplicationException.java index f851d67c..c4f02647 100644 --- a/src/main/java/poomasi/global/error/ApplicationException.java +++ b/src/main/java/poomasi/global/error/ApplicationException.java @@ -6,5 +6,6 @@ @Getter @AllArgsConstructor public class ApplicationException extends RuntimeException { + private final ApplicationError applicationError; } diff --git a/src/main/java/poomasi/global/error/BusinessException.java b/src/main/java/poomasi/global/error/BusinessException.java index 0bf9077b..170c7344 100644 --- a/src/main/java/poomasi/global/error/BusinessException.java +++ b/src/main/java/poomasi/global/error/BusinessException.java @@ -6,5 +6,6 @@ @AllArgsConstructor @Getter public class BusinessException extends RuntimeException { + private final BusinessError businessError; } From cce95750b84d8713a4f15310e5637f85b152cfe3 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Fri, 4 Oct 2024 03:38:20 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20-=20=EC=83=81=ED=92=88=EB=A6=AC=EB=B7=B0=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=97=B0=EA=B4=80=20=EA=B4=80=EA=B3=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 17 +++++++---- .../domain/product/dto/ProductResponse.java | 30 +++++++++++++++---- .../domain/product/entity/Product.java | 2 +- .../product/service/ProductService.java | 26 ++++++++++++++++ .../review/dto/ProductReviewResponse.java | 8 +++-- .../domain/review/entity/ProductReview.java | 9 +++++- .../review/entity/ProductReviewPhoto.java | 11 ++++++- .../ProductReviewPhotoRepository.java | 10 +++++++ .../service/ProductReviewCustomerService.java | 15 ++++++++++ 9 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 src/main/java/poomasi/domain/product/service/ProductService.java create mode 100644 src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java diff --git a/src/main/java/poomasi/domain/product/controller/ProductController.java b/src/main/java/poomasi/domain/product/controller/ProductController.java index e0f57848..04262629 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductController.java @@ -1,18 +1,23 @@ package poomasi.domain.product.controller; import lombok.RequiredArgsConstructor; +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.RestController; -import poomasi.domain.product.service.ProductAdminService; +import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.product.service.ProductService; @RestController @RequiredArgsConstructor public class ProductController { - private final ProductAdminService productAdminService; + private final ProductService productService; -// @GetMapping("/api/products/{productId") -// public ResponseEntity getProducts(@PathVariable int productId) { -// -// } + @GetMapping("/api/products/{productId}") + public ResponseEntity getProductsDetail(@PathVariable long productId) { + ProductResponse productResponse = productService.getProductDetail(productId); + return ResponseEntity.ok(productResponse); + } } diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index c49bb611..69a37460 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -2,31 +2,49 @@ import java.util.List; import poomasi.domain.product.entity.Product; -import poomasi.domain.review.entity.ProductReview; +import poomasi.domain.review.dto.ProductReviewResponse; public record ProductResponse( long id, long categoryId, - long farmerId, + //long farmerId, String name, String description, String imageUrl, int quantity, int price, - List reviewList + List reviewList, + float averageRating ) { - public ProductResponse fromEntity(Product product) { + public static ProductResponse fromEntity(Product product) { + final float[] sum = {0.0f}; + final int[] cnt = {0}; + + List reviews = product.getReviewList().stream() + .map(productReview -> { + sum[0] += productReview.getRating(); // 평점 합계 계산 + cnt[0]++; // 리뷰 개수 증가 + return ProductReviewResponse.fromEntity(productReview); // 각 리뷰를 응답 객체로 변환 + }) + .toList(); + + float averageRating = 0.0f; + if (cnt[0] > 0) { + averageRating = (sum[0] / cnt[0]); + } + return new ProductResponse( product.getId(), product.getCategory().getId(), - product.getFarmerId(), + //product.getFarmerId(), product.getName(), product.getDescription(), product.getImageUrl(), product.getQuantity(), product.getPrice(), - product.getReviewList() + reviews, + averageRating ); } } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index fa3ece04..7a997af1 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -63,7 +63,7 @@ public class Product { @UpdateTimestamp private LocalDateTime updatedAt; - @OneToMany(mappedBy = "product_review", cascade = CascadeType.REMOVE, orphanRemoval = true) + @OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, orphanRemoval = true) List reviewList = new ArrayList<>(); @Builder diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java new file mode 100644 index 00000000..6f2f4fde --- /dev/null +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -0,0 +1,26 @@ +package poomasi.domain.product.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + private Product getProductById(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + } + + public ProductResponse getProductDetail(long productId) { + Product product = getProductById(productId); + return ProductResponse.fromEntity(product); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java index 255c1947..e3a40eb2 100644 --- a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java +++ b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java @@ -1,13 +1,16 @@ package poomasi.domain.review.dto; +import java.util.List; import poomasi.domain.review.entity.ProductReview; +import poomasi.domain.review.entity.ProductReviewPhoto; public record ProductReviewResponse (long id, long productId, //long reviewerId, float rating, - String content + String content, + List imageUrls ) { public static ProductReviewResponse fromEntity(ProductReview productReview) { @@ -16,7 +19,8 @@ public static ProductReviewResponse fromEntity(ProductReview productReview) { productReview.getProduct().getId(), //productReview.getReviewer().getId(), productReview.getRating(), - productReview.getContent() + productReview.getContent(), + productReview.getImageUrl().stream().map(ProductReviewPhoto::getUrl).toList() ); } } diff --git a/src/main/java/poomasi/domain/review/entity/ProductReview.java b/src/main/java/poomasi/domain/review/entity/ProductReview.java index 6014dc1a..8ea3a68a 100644 --- a/src/main/java/poomasi/domain/review/entity/ProductReview.java +++ b/src/main/java/poomasi/domain/review/entity/ProductReview.java @@ -42,7 +42,7 @@ public class ProductReview { @ManyToOne(fetch = FetchType.LAZY) private Product product; - @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + @OneToMany(mappedBy = "productReview", cascade = CascadeType.REMOVE, orphanRemoval = true) private List imageUrl = new ArrayList<>(); // @Comment("작성자") @@ -54,4 +54,11 @@ public ProductReview(float rating, String content, Product product) { this.content = content; this.product = product; } + + public ProductReviewPhoto addPhoto(String url) { + ProductReviewPhoto productReviewPhoto = new ProductReviewPhoto(this, url); + productReviewPhoto.setReview(this); + imageUrl.add(productReviewPhoto); + return productReviewPhoto; + } } diff --git a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java index 7d692b68..b5a498c3 100644 --- a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java +++ b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java @@ -1,6 +1,7 @@ package poomasi.domain.review.entity; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -19,10 +20,18 @@ public class ProductReviewPhoto { private long id; @Comment("리뷰") - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private ProductReview productReview; @Comment("사진 경로") private String url; + public ProductReviewPhoto(ProductReview productReview, String url) { + this.productReview = productReview; + this.url = url; + } + + public void setReview(ProductReview productReview) { + this.productReview = productReview; + } } diff --git a/src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java b/src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java new file mode 100644 index 00000000..36937c68 --- /dev/null +++ b/src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java @@ -0,0 +1,10 @@ +package poomasi.domain.review.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.review.entity.ProductReviewPhoto; + +@Repository +public interface ProductReviewPhotoRepository extends JpaRepository { + +} diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java index e487fcd5..bb192b33 100644 --- a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java +++ b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java @@ -7,6 +7,8 @@ import poomasi.domain.product.repository.ProductRepository; import poomasi.domain.review.dto.ProductReviewRequest; import poomasi.domain.review.entity.ProductReview; +import poomasi.domain.review.entity.ProductReviewPhoto; +import poomasi.domain.review.repository.ProductReviewPhotoRepository; import poomasi.domain.review.repository.ProductReviewRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -17,13 +19,26 @@ public class ProductReviewCustomerService { private final ProductReviewRepository productReviewRepository; private final ProductRepository productRepository; + private final ProductReviewPhotoRepository productReviewPhotoRepository; public long registerReview(long productId, ProductReviewRequest productReviewRequest) { //이미지 저장하고 주소 받아와서 review에 추가해주기 + String url1 = "test1"; + String url2 = "test2"; + String url3 = "test3"; + Product product = getProductByProductId(productId); ProductReview pReview = productReviewRequest.toEntity(product); pReview = productReviewRepository.save(pReview); product.addReview(pReview); + + ProductReviewPhoto reviewPhoto1 = pReview.addPhoto(url1); + productReviewPhotoRepository.save(reviewPhoto1); + ProductReviewPhoto reviewPhoto2 = pReview.addPhoto(url2); + productReviewPhotoRepository.save(reviewPhoto2); + ProductReviewPhoto reviewPhoto3 = pReview.addPhoto(url3); + productReviewPhotoRepository.save(reviewPhoto3); + return pReview.getId(); } From 80eed5b0244c5ec36bc4ec551052c9e950eed3f0 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Fri, 4 Oct 2024 04:40:55 +0900 Subject: [PATCH 07/17] =?UTF-8?q?refactor:=20=ED=8F=89=EA=B7=A0=20?= =?UTF-8?q?=ED=8F=89=EC=A0=90=EC=9D=84=20=EB=A6=AC=EB=B7=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=A0=20=EB=95=8C=20=EA=B3=84=EC=82=B0=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/dto/ProductResponse.java | 21 +++---------------- .../domain/product/entity/Product.java | 7 +++++++ .../ProductReviewCustomerController.java | 7 +++++++ .../domain/review/entity/ProductReview.java | 6 ++++++ .../service/ProductReviewCustomerService.java | 7 +++++++ 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 69a37460..593bdc71 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -14,25 +14,10 @@ public record ProductResponse( int quantity, int price, List reviewList, - float averageRating + double averageRating ) { public static ProductResponse fromEntity(Product product) { - final float[] sum = {0.0f}; - final int[] cnt = {0}; - - List reviews = product.getReviewList().stream() - .map(productReview -> { - sum[0] += productReview.getRating(); // 평점 합계 계산 - cnt[0]++; // 리뷰 개수 증가 - return ProductReviewResponse.fromEntity(productReview); // 각 리뷰를 응답 객체로 변환 - }) - .toList(); - - float averageRating = 0.0f; - if (cnt[0] > 0) { - averageRating = (sum[0] / cnt[0]); - } return new ProductResponse( product.getId(), @@ -43,8 +28,8 @@ public static ProductResponse fromEntity(Product product) { product.getImageUrl(), product.getQuantity(), product.getPrice(), - reviews, - averageRating + product.getReviewList().stream().map(ProductReviewResponse::fromEntity).toList(), + product.getAverageRating() ); } } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 7a997af1..17cc6e3b 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -66,6 +66,9 @@ public class Product { @OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, orphanRemoval = true) List reviewList = new ArrayList<>(); + @Comment("평균 평점") + private double averageRating=0.0; + @Builder public Product(Long productId, Category category, @@ -100,6 +103,10 @@ public void addQuantity(Integer quantity) { public void addReview(ProductReview pReview) { this.reviewList.add(pReview); + this.averageRating = reviewList.stream() + .mapToDouble(ProductReview::getRating) // 각 리뷰의 평점을 double로 변환 + .average() // 평균 계산 + .orElse(0.0); } } diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java index c009943f..92a8da0c 100644 --- a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java +++ b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; 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 poomasi.domain.review.dto.ProductReviewRequest; import poomasi.domain.review.service.ProductReviewCustomerService; @@ -30,4 +31,10 @@ public ResponseEntity deleteProductReview(@PathVariable long reviewId) { productReviewCustomerService.deleteReview(reviewId); return new ResponseEntity<>(HttpStatus.OK); } + + @PutMapping("/api/reviews/{reviewId}") + public ResponseEntity modifyProductReview(@PathVariable long reviewId, @RequestBody ProductReviewRequest productReviewRequest){ + productReviewCustomerService.modifyReview(reviewId, productReviewRequest); + return new ResponseEntity<>(HttpStatus.OK); + } } diff --git a/src/main/java/poomasi/domain/review/entity/ProductReview.java b/src/main/java/poomasi/domain/review/entity/ProductReview.java index 8ea3a68a..d96c013b 100644 --- a/src/main/java/poomasi/domain/review/entity/ProductReview.java +++ b/src/main/java/poomasi/domain/review/entity/ProductReview.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.product.entity.Product; +import poomasi.domain.review.dto.ProductReviewRequest; @Entity @Getter @@ -61,4 +62,9 @@ public ProductReviewPhoto addPhoto(String url) { imageUrl.add(productReviewPhoto); return productReviewPhoto; } + + public void modifyReview(ProductReviewRequest productReviewRequest) { + this.rating = productReviewRequest.rating(); + this.content = productReviewRequest.content(); + } } diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java index bb192b33..557f4ded 100644 --- a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java +++ b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java @@ -21,6 +21,7 @@ public class ProductReviewCustomerService { private final ProductRepository productRepository; private final ProductReviewPhotoRepository productReviewPhotoRepository; + @Transactional public long registerReview(long productId, ProductReviewRequest productReviewRequest) { //이미지 저장하고 주소 받아와서 review에 추가해주기 String url1 = "test1"; @@ -58,4 +59,10 @@ private ProductReview getReviewById(long reviewId) { return productReviewRepository.findById(reviewId) .orElseThrow(() -> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); } + + @Transactional + public void modifyReview(long reviewId, ProductReviewRequest productReviewRequest) { + ProductReview pReview = getReviewById(reviewId); + pReview.modifyReview(productReviewRequest); + } } From 40c9d2573c3b483cfe7c637921a8a38ece3b63fc Mon Sep 17 00:00:00 2001 From: stopmin Date: Fri, 4 Oct 2024 16:14:21 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../category/service/CategoryService.java | 38 ------------------- .../controller/CategoryController.java | 2 + .../_category/dto/CategoryResponse.java | 7 +++- .../dto/ProductListInCategoryResponse.java | 23 +++++------ .../product/_category/entity/Category.java | 3 ++ .../_category/service/CategoryService.java | 12 +++++- .../controller/ProductAdminController.java | 3 +- .../domain/product/entity/Product.java | 12 +++--- .../product/service/ProductFarmerService.java | 17 +++++---- 9 files changed, 51 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/poomasi/domain/category/service/CategoryService.java diff --git a/src/main/java/poomasi/domain/category/service/CategoryService.java b/src/main/java/poomasi/domain/category/service/CategoryService.java deleted file mode 100644 index c4fd2889..00000000 --- a/src/main/java/poomasi/domain/category/service/CategoryService.java +++ /dev/null @@ -1,38 +0,0 @@ -package poomasi.domain.category.service; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import poomasi.domain.category.dto.CategoryResponse; -import poomasi.domain.category.entity.Category; -import poomasi.domain.category.repository.CategoryRepository; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; -import poomasi.domain.category.dto.ProductListInCategoryResponse; - - - -@Service -@RequiredArgsConstructor -public class CategoryService { - - private final CategoryRepository categoryRepository; - - public List getAllCategories() { - List categories = categoryRepository.findAll(); - return categories.stream() - .map(CategoryResponse::fromEntity) - .toList(); - } - - public List getProductInCategory(long categoryId) { - Category category = getCategory(categoryId); - return category.getProducts().stream() - .map(ProductListInCategoryResponse::fromEntity).toList(); - } - - private Category getCategory(long categoryId) { - return categoryRepository.findById(categoryId) - .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); - } -} diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java index f9445066..46777ae7 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java @@ -1,6 +1,7 @@ package poomasi.domain.product._category.controller; import java.util.List; + import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -8,6 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import poomasi.domain.product._category.dto.CategoryResponse; +import poomasi.domain.product._category.dto.ProductListInCategoryResponse; import poomasi.domain.product._category.service.CategoryService; @RestController diff --git a/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java index 3e46dcac..db1b4835 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java +++ b/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java @@ -1,10 +1,15 @@ package poomasi.domain.product._category.dto; +import lombok.Builder; import poomasi.domain.product._category.entity.Category; +@Builder public record CategoryResponse(long id, String name) { public static CategoryResponse fromEntity(Category category) { - return new CategoryResponse(category.getId(), category.getName()); + return CategoryResponse.builder() + .id(category.getId()) + .name(category.getName()) + .build(); } } diff --git a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java index 47f49eb8..ecfba89e 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java +++ b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java @@ -1,25 +1,26 @@ -package poomasi.domain.category.dto; +package poomasi.domain.product._category.dto; +import lombok.Builder; import poomasi.domain.product.entity.Product; +@Builder public record ProductListInCategoryResponse( Long categoryId, - //Long farmerId, //등록한 사람 String name, String description, String imageUrl, int quantity, - int price + Long price ) { public static ProductListInCategoryResponse fromEntity(Product product) { - return new ProductListInCategoryResponse( - product.getCategory().getId(), - product.getName(), - product.getDescription(), - product.getImageUrl(), - product.getQuantity(), - product.getPrice() - ); + return ProductListInCategoryResponse.builder() + .categoryId(product.getCategoryId()) + .name(product.getName()) + .description(product.getDescription()) + .imageUrl(product.getImageUrl()) + .quantity(product.getStock()) + .price(product.getPrice()) + .build(); } } diff --git a/src/main/java/poomasi/domain/product/_category/entity/Category.java b/src/main/java/poomasi/domain/product/_category/entity/Category.java index a364e5b1..57a6a303 100644 --- a/src/main/java/poomasi/domain/product/_category/entity/Category.java +++ b/src/main/java/poomasi/domain/product/_category/entity/Category.java @@ -6,11 +6,14 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; + import java.util.ArrayList; import java.util.List; + import lombok.Getter; import lombok.NoArgsConstructor; import poomasi.domain.product._category.dto.CategoryRequest; +import poomasi.domain.product.entity.Product; @Entity @Getter diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java index f6b5dfce..ee72c924 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product._category.dto.CategoryResponse; +import poomasi.domain.product._category.dto.ProductListInCategoryResponse; import poomasi.domain.product._category.entity.Category; import poomasi.domain.product._category.repository.CategoryRepository; import poomasi.global.error.BusinessError; @@ -23,9 +24,18 @@ public List getAllCategories() { .toList(); } + public List getProductInCategory(long categoryId) { + Category category = getCategory(categoryId); + return category.getProducts() + .stream() + .map(ProductListInCategoryResponse::fromEntity) + .toList(); + } + public Category getCategory(Long categoryId) { - return categoryRepository.findById(categoryId) + return categoryRepository.findById(categoryId) .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); } + } diff --git a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java index fe0b5380..512e93f3 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java @@ -18,7 +18,8 @@ public class ProductAdminController { @PutMapping("/{productId}/open") ResponseEntity openProduct(@PathVariable Long productId) { - productAdminService.openProduct(productId); + // FIXME: 2024-10-04 +// productAdminService.openProduct(productId); return new ResponseEntity<>(productId, HttpStatus.OK); } } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 97c969bb..5abad959 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,9 +1,6 @@ package poomasi.domain.product.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,8 +9,11 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.product.dto.ProductRegisterRequest; +import poomasi.domain.review.entity.ProductReview; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -58,12 +58,12 @@ public class Product { List reviewList = new ArrayList<>(); @Comment("평균 평점") - private double averageRating=0.0; + private double averageRating = 0.0; @Builder public Product(Long productId, Long categoryId, - Long farmerId, //등록한 사람 + Long farmerId, String name, String description, String imageUrl, diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index 5e818dc3..13822b30 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import poomasi.domain.member.service.MemberService; +import poomasi.domain.product._category.entity.Category; import poomasi.domain.product._category.service.CategoryService; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; @@ -28,28 +29,28 @@ public Long registerProduct(ProductRegisterRequest request) { return saveProduct.getId(); } - private Category getCategory(ProductRegisterRequest product) { - return categoryRepository.findById(product.categoryId()) - .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); - } @Transactional public void modifyProduct(ProductRegisterRequest productRequest, Long productId) { // TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); - product.getCategory().deleteProduct(product); //원래 카테고리에서 상품 삭제 + + // FIXME: 이거 수정해야할듯? + /* product.getCategory().deleteProduct(product); //원래 카테고리에서 상품 삭제 product = productRepository.save(product.modify(category, productRequest)); //상품 갱신 - category.addProduct(product);//새로운 카테고리에 추가 + category.addProduct(product);//새로운 카테고리에 추가*/ } @Transactional public void deleteProduct(Long productId) { //TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); - Category category = product.getCategory(); - category.deleteProduct(product); + // FIXME: 이거 수정해야할듯? + /* Category category = product.getCategory(); + + category.deleteProduct(product);*/ productRepository.delete(product); } From 4e5c86cef0d2f1b5d2478c79f1a269fa75f74da1 Mon Sep 17 00:00:00 2001 From: stopmin Date: Fri, 4 Oct 2024 16:19:16 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/poomasi/Application.java | 1 - src/main/java/poomasi/domain/farm/controller/FarmController.java | 1 - .../poomasi/domain/farm/controller/FarmFarmerController.java | 1 - src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java | 1 - src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java | 1 - src/main/java/poomasi/domain/farm/repository/FarmRepository.java | 1 - .../domain/product/_category/service/CategoryAdminService.java | 1 - .../domain/product/controller/ProductAdminController.java | 1 - .../java/poomasi/domain/product/service/ProductAdminService.java | 1 - 9 files changed, 9 deletions(-) diff --git a/src/main/java/poomasi/Application.java b/src/main/java/poomasi/Application.java index d8253f19..a8175566 100644 --- a/src/main/java/poomasi/Application.java +++ b/src/main/java/poomasi/Application.java @@ -8,7 +8,6 @@ @EnableJpaAuditing @SpringBootApplication(exclude = SecurityAutoConfiguration.class) public class Application { - public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/src/main/java/poomasi/domain/farm/controller/FarmController.java b/src/main/java/poomasi/domain/farm/controller/FarmController.java index 7bb470f0..cd2899f2 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmController.java @@ -14,7 +14,6 @@ @RequiredArgsConstructor @RequestMapping("/api/farm") public class FarmController { - private final FarmService farmService; @GetMapping("/{farmId}") diff --git a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java index d2aa4cf9..eed59cd3 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java @@ -13,7 +13,6 @@ @RequiredArgsConstructor @RequestMapping("/api/farm") public class FarmFarmerController { - private final FarmFarmerService farmFarmerService; private final FarmScheduleService farmScheduleService; diff --git a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java index 8f104363..49447af8 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java @@ -13,7 +13,6 @@ public record FarmRegisterRequest( String description, Long experiencePrice ) { - public Farm toEntity() { return Farm.builder() .name(name) diff --git a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java index fb746802..23e5ebbe 100644 --- a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java +++ b/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java @@ -12,7 +12,6 @@ public record FarmUpdateRequest( Double latitude, Double longitude ) { - public Farm toEntity(Farm farm) { return farm.updateFarm(this); } diff --git a/src/main/java/poomasi/domain/farm/repository/FarmRepository.java b/src/main/java/poomasi/domain/farm/repository/FarmRepository.java index 3b763bfa..96b40c67 100644 --- a/src/main/java/poomasi/domain/farm/repository/FarmRepository.java +++ b/src/main/java/poomasi/domain/farm/repository/FarmRepository.java @@ -10,7 +10,6 @@ @Repository public interface FarmRepository extends JpaRepository { - Page findAll(Pageable pageable); Page findByDeletedAtIsNull(Pageable pageable); diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java index f99c1c46..b2e12c64 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java @@ -12,7 +12,6 @@ @Service @RequiredArgsConstructor public class CategoryAdminService { - private final CategoryRepository categoryRepository; public Long registerCategory(CategoryRequest categoryRequest) { diff --git a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java index 512e93f3..efecf945 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java @@ -13,7 +13,6 @@ @RequiredArgsConstructor @RequestMapping("/api/product") public class ProductAdminController { - private final ProductAdminService productAdminService; @PutMapping("/{productId}/open") diff --git a/src/main/java/poomasi/domain/product/service/ProductAdminService.java b/src/main/java/poomasi/domain/product/service/ProductAdminService.java index 86a59f5a..1c7a73ee 100644 --- a/src/main/java/poomasi/domain/product/service/ProductAdminService.java +++ b/src/main/java/poomasi/domain/product/service/ProductAdminService.java @@ -10,7 +10,6 @@ @Service @RequiredArgsConstructor public class ProductAdminService { - private final ProductRepository productRepository; private Product getProductByProductId(Long productId) { From 9b81a301ed2751022dd2987bf5ae7a026d2a3e49 Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Mon, 7 Oct 2024 22:03:39 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20review=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CategoryController.java | 2 +- .../_category/dto/CategoryResponse.java | 2 +- .../dto/ProductListInCategoryResponse.java | 2 +- .../product/_category/entity/Category.java | 6 +- .../_category/service/CategoryService.java | 2 +- .../controller/ProductAdminController.java | 24 ------- .../product/dto/ProductListResponse.java | 5 -- .../product/dto/ProductRegisterRequest.java | 3 +- .../domain/product/dto/ProductResponse.java | 8 ++- .../domain/product/entity/Product.java | 17 ++--- .../product/service/ProductAdminService.java | 20 ------ .../product/service/ProductFarmerService.java | 36 ++++++---- .../controller/ProductReviewController.java | 24 ------- .../ProductReviewCustomerController.java | 40 ----------- .../review/controller/ReviewController.java | 29 ++++++++ .../controller/farm/FarmReviewController.java | 35 ++++++++++ .../product/ProductReviewController.java | 35 ++++++++++ .../review/dto/ProductReviewRequest.java | 14 ---- .../review/dto/ProductReviewResponse.java | 26 ------- .../domain/review/dto/ReviewRequest.java | 13 ++++ .../domain/review/dto/ReviewResponse.java | 24 +++++++ .../domain/review/entity/EntityType.java | 6 ++ .../domain/review/entity/ProductReview.java | 70 ------------------- .../review/entity/ProductReviewPhoto.java | 37 ---------- .../poomasi/domain/review/entity/Review.java | 63 +++++++++++++++++ .../ProductReviewPhotoRepository.java | 10 --- .../repository/ProductReviewRepository.java | 14 ---- .../review/repository/ReviewRepository.java | 17 +++++ .../service/ProductReviewCustomerService.java | 68 ------------------ .../review/service/ProductReviewService.java | 33 --------- .../domain/review/service/ReviewService.java | 33 +++++++++ .../service/farm/FarmReviewService.java | 47 +++++++++++++ .../service/product/ProductReviewService.java | 47 +++++++++++++ 33 files changed, 394 insertions(+), 418 deletions(-) delete mode 100644 src/main/java/poomasi/domain/product/controller/ProductAdminController.java delete mode 100644 src/main/java/poomasi/domain/product/dto/ProductListResponse.java delete mode 100644 src/main/java/poomasi/domain/product/service/ProductAdminService.java delete mode 100644 src/main/java/poomasi/domain/review/controller/ProductReviewController.java delete mode 100644 src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java create mode 100644 src/main/java/poomasi/domain/review/controller/ReviewController.java create mode 100644 src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java create mode 100644 src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java delete mode 100644 src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java delete mode 100644 src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java create mode 100644 src/main/java/poomasi/domain/review/dto/ReviewRequest.java create mode 100644 src/main/java/poomasi/domain/review/dto/ReviewResponse.java create mode 100644 src/main/java/poomasi/domain/review/entity/EntityType.java delete mode 100644 src/main/java/poomasi/domain/review/entity/ProductReview.java delete mode 100644 src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java create mode 100644 src/main/java/poomasi/domain/review/entity/Review.java delete mode 100644 src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java delete mode 100644 src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java create mode 100644 src/main/java/poomasi/domain/review/repository/ReviewRepository.java delete mode 100644 src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java delete mode 100644 src/main/java/poomasi/domain/review/service/ProductReviewService.java create mode 100644 src/main/java/poomasi/domain/review/service/ReviewService.java create mode 100644 src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java create mode 100644 src/main/java/poomasi/domain/review/service/product/ProductReviewService.java diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java index 46777ae7..4c7730fa 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java @@ -25,7 +25,7 @@ public ResponseEntity getAllCategories() { } @GetMapping("/api/categories/{categoryId}") - public ResponseEntity getCategoryById(@PathVariable long categoryId) { + public ResponseEntity getCategoryById(@PathVariable Long categoryId) { List productList = categoryService.getProductInCategory( categoryId); return new ResponseEntity<>(productList, HttpStatus.OK); diff --git a/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java index db1b4835..efaadcb8 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java +++ b/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java @@ -4,7 +4,7 @@ import poomasi.domain.product._category.entity.Category; @Builder -public record CategoryResponse(long id, String name) { +public record CategoryResponse(Long id, String name) { public static CategoryResponse fromEntity(Category category) { return CategoryResponse.builder() diff --git a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java index ecfba89e..12eff02f 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java +++ b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java @@ -9,7 +9,7 @@ public record ProductListInCategoryResponse( String name, String description, String imageUrl, - int quantity, + Integer quantity, Long price ) { diff --git a/src/main/java/poomasi/domain/product/_category/entity/Category.java b/src/main/java/poomasi/domain/product/_category/entity/Category.java index 57a6a303..e256f0c7 100644 --- a/src/main/java/poomasi/domain/product/_category/entity/Category.java +++ b/src/main/java/poomasi/domain/product/_category/entity/Category.java @@ -5,6 +5,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; import java.util.ArrayList; @@ -21,10 +22,11 @@ public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + private Long id; private String name; - @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "categoryId") List products = new ArrayList<>(); public Category(String name) { diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java index ee72c924..18ea8731 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java @@ -24,7 +24,7 @@ public List getAllCategories() { .toList(); } - public List getProductInCategory(long categoryId) { + public List getProductInCategory(Long categoryId) { Category category = getCategory(categoryId); return category.getProducts() .stream() diff --git a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java deleted file mode 100644 index efecf945..00000000 --- a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java +++ /dev/null @@ -1,24 +0,0 @@ -package poomasi.domain.product.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import poomasi.domain.product.service.ProductAdminService; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/product") -public class ProductAdminController { - private final ProductAdminService productAdminService; - - @PutMapping("/{productId}/open") - ResponseEntity openProduct(@PathVariable Long productId) { - // FIXME: 2024-10-04 -// productAdminService.openProduct(productId); - return new ResponseEntity<>(productId, HttpStatus.OK); - } -} diff --git a/src/main/java/poomasi/domain/product/dto/ProductListResponse.java b/src/main/java/poomasi/domain/product/dto/ProductListResponse.java deleted file mode 100644 index 3faeeb79..00000000 --- a/src/main/java/poomasi/domain/product/dto/ProductListResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package poomasi.domain.product.dto; - -public class ProductListResponse { - -} diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java index 95c0189d..96278af7 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java @@ -8,7 +8,7 @@ public record ProductRegisterRequest( String name, String description, String imageUrl, - int stock, + Integer stock, Long price ) { @@ -17,6 +17,7 @@ public Product toEntity() { .categoryId(categoryId) .farmerId(farmerId) .name(name) + .stock(stock) .description(description) .imageUrl(imageUrl) .stock(stock) diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 75db2432..1745b503 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -1,17 +1,19 @@ package poomasi.domain.product.dto; +import java.util.List; import lombok.Builder; import poomasi.domain.product.entity.Product; +import poomasi.domain.review.dto.ReviewResponse; @Builder public record ProductResponse( - long id, + Long id, String name, Long price, - int stock, + Integer stock, String description, String imageUrl, - long categoryId + Long categoryId ) { public static ProductResponse fromEntity(Product product) { return ProductResponse.builder() diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 5abad959..e4db9e1b 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -9,7 +9,7 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.product.dto.ProductRegisterRequest; -import poomasi.domain.review.entity.ProductReview; +import poomasi.domain.review.entity.Review; import java.time.LocalDateTime; import java.util.ArrayList; @@ -40,7 +40,7 @@ public class Product { private String imageUrl; @Comment("재고") - private int stock; + private Integer stock; @Comment("가격") private Long price; @@ -54,8 +54,9 @@ public class Product { @UpdateTimestamp private LocalDateTime updatedAt; - @OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, orphanRemoval = true) - List reviewList = new ArrayList<>(); + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + @JoinColumn(name = "entityId") + List reviewList = new ArrayList<>(); @Comment("평균 평점") private double averageRating = 0.0; @@ -67,7 +68,7 @@ public Product(Long productId, String name, String description, String imageUrl, - int stock, + Integer stock, Long price) { this.categoryId = categoryId; this.farmerId = farmerId; @@ -88,14 +89,14 @@ public Product modify(ProductRegisterRequest productRegisterRequest) { return this; } - public void addQuantity(int stock) { + public void addStock (Integer stock) { this.stock += stock; } - public void addReview(ProductReview pReview) { + public void addReview(Review pReview) { this.reviewList.add(pReview); this.averageRating = reviewList.stream() - .mapToDouble(ProductReview::getRating) // 각 리뷰의 평점을 double로 변환 + .mapToDouble(Review::getRating) // 각 리뷰의 평점을 double로 변환 .average() // 평균 계산 .orElse(0.0); } diff --git a/src/main/java/poomasi/domain/product/service/ProductAdminService.java b/src/main/java/poomasi/domain/product/service/ProductAdminService.java deleted file mode 100644 index 1c7a73ee..00000000 --- a/src/main/java/poomasi/domain/product/service/ProductAdminService.java +++ /dev/null @@ -1,20 +0,0 @@ -package poomasi.domain.product.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; - -@Service -@RequiredArgsConstructor -public class ProductAdminService { - private final ProductRepository productRepository; - - private Product getProductByProductId(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); - } - -} diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index 13822b30..43c3091a 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -3,9 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.member.service.MemberService; import poomasi.domain.product._category.entity.Category; -import poomasi.domain.product._category.service.CategoryService; +import poomasi.domain.product._category.repository.CategoryRepository; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.entity.Product; @@ -18,14 +17,15 @@ public class ProductFarmerService { private final ProductRepository productRepository; - private final CategoryService categoryService; - private final MemberService memberService; + private final CategoryRepository categoryRepository; + //private final MemberService memberService; public Long registerProduct(ProductRegisterRequest request) { - memberService.isFarmer(request.farmerId()); - categoryService.getCategory(request.categoryId()); + //memberService.isFarmer(request.farmerId()); + Category category = getCategory(request.categoryId()); Product saveProduct = productRepository.save(request.toEntity()); + category.addProduct(saveProduct); return saveProduct.getId(); } @@ -34,30 +34,32 @@ public Long registerProduct(ProductRegisterRequest request) { public void modifyProduct(ProductRegisterRequest productRequest, Long productId) { // TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); + Long categoryId = product.getCategoryId(); + Category oldCategory = getCategory(categoryId); + oldCategory.deleteProduct(product);//원래 카테고리에서 삭제 + product = productRepository.save(product.modify(productRequest)); //상품 갱신 - // FIXME: 이거 수정해야할듯? - /* product.getCategory().deleteProduct(product); //원래 카테고리에서 상품 삭제 - product = productRepository.save(product.modify(category, productRequest)); //상품 갱신 - category.addProduct(product);//새로운 카테고리에 추가*/ + categoryId = productRequest.categoryId(); + Category newCategory = getCategory(categoryId); + newCategory.addProduct(product); } @Transactional public void deleteProduct(Long productId) { //TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); + Long categoryId = product.getCategoryId(); + Category category = getCategory(categoryId); - // FIXME: 이거 수정해야할듯? - /* Category category = product.getCategory(); - - category.deleteProduct(product);*/ + category.deleteProduct(product); productRepository.delete(product); } @Transactional public void addQuantity(Long productId, UpdateProductQuantityRequest request) { Product productByProductId = getProductByProductId(productId); - productByProductId.addQuantity(request.quantity()); + productByProductId.addStock(request.quantity()); productRepository.save(productByProductId); } @@ -66,4 +68,8 @@ private Product getProductByProductId(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } + + private Category getCategory(Long categoryId){ + return categoryRepository.findById(categoryId).orElseThrow(()->new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + } } diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewController.java deleted file mode 100644 index b7387e2c..00000000 --- a/src/main/java/poomasi/domain/review/controller/ProductReviewController.java +++ /dev/null @@ -1,24 +0,0 @@ -package poomasi.domain.review.controller; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import poomasi.domain.review.dto.ProductReviewResponse; -import poomasi.domain.review.service.ProductReviewService; - -@Controller -@RequiredArgsConstructor -public class ProductReviewController { - - private final ProductReviewService productReviewService; - - @GetMapping("/api/products/{productId}/reviews") - public ResponseEntity getProductReviews(@PathVariable long productId) { - List response = productReviewService.getProductReview(productId); - return new ResponseEntity<>(response, HttpStatus.OK); - } -} diff --git a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java b/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java deleted file mode 100644 index 92a8da0c..00000000 --- a/src/main/java/poomasi/domain/review/controller/ProductReviewCustomerController.java +++ /dev/null @@ -1,40 +0,0 @@ -package poomasi.domain.review.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.DeleteMapping; -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 poomasi.domain.review.dto.ProductReviewRequest; -import poomasi.domain.review.service.ProductReviewCustomerService; - -@Controller -@RequiredArgsConstructor -public class ProductReviewCustomerController { - - private final ProductReviewCustomerService productReviewCustomerService; - - @PostMapping("/api/products/{productId}/reviews") - public ResponseEntity registerProductReview(@PathVariable int productId, - @RequestBody ProductReviewRequest productReviewRequest) { - long reviewId = productReviewCustomerService.registerReview(productId, - productReviewRequest); - return new ResponseEntity<>(reviewId, HttpStatus.CREATED); - } - - @DeleteMapping("/api/reviews/{reviewId}") - public ResponseEntity deleteProductReview(@PathVariable long reviewId) { - productReviewCustomerService.deleteReview(reviewId); - return new ResponseEntity<>(HttpStatus.OK); - } - - @PutMapping("/api/reviews/{reviewId}") - public ResponseEntity modifyProductReview(@PathVariable long reviewId, @RequestBody ProductReviewRequest productReviewRequest){ - productReviewCustomerService.modifyReview(reviewId, productReviewRequest); - return new ResponseEntity<>(HttpStatus.OK); - } -} diff --git a/src/main/java/poomasi/domain/review/controller/ReviewController.java b/src/main/java/poomasi/domain/review/controller/ReviewController.java new file mode 100644 index 00000000..dae1b3da --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/ReviewController.java @@ -0,0 +1,29 @@ +package poomasi.domain.review.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.service.ReviewService; + +@Controller +@RequiredArgsConstructor +public class ReviewController { + private final ReviewService reviewService; + @DeleteMapping("/api/reviews/{reviewId}") + public ResponseEntity deleteProductReview(@PathVariable Long reviewId) { + reviewService.deleteReview(reviewId); + return new ResponseEntity<>(HttpStatus.OK); + } + + @PutMapping("/api/reviews/{reviewId}") + public ResponseEntity modifyProductReview(@PathVariable Long reviewId, @RequestBody ReviewRequest reviewRequest){ + reviewService.modifyReview(reviewId, reviewRequest); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java new file mode 100644 index 00000000..83e3f79f --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java @@ -0,0 +1,35 @@ +package poomasi.domain.review.controller.farm; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +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.RequestBody; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.entity.EntityType; +import poomasi.domain.review.service.farm.FarmReviewService; + +@Controller +@RequiredArgsConstructor +public class FarmReviewController { + private final FarmReviewService farmReviewService; + + @GetMapping("/api/farm/{farmId}/reviews") + public ResponseEntity getProductReviews(@PathVariable Long farmId) { + List response = farmReviewService.getFarmReview(farmId); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PostMapping("/api/farm/{farmId}/reviews") + public ResponseEntity registerProductReview(@PathVariable Long farmId, + @RequestBody ReviewRequest reviewRequest) { + Long reviewId = farmReviewService.registerFarmReview(farmId, + reviewRequest); + return new ResponseEntity<>(reviewId, HttpStatus.CREATED); + } +} diff --git a/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java new file mode 100644 index 00000000..98fd6249 --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java @@ -0,0 +1,35 @@ +package poomasi.domain.review.controller.product; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +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.RequestBody; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.service.product.ProductReviewService; + +@Controller +@RequiredArgsConstructor +public class ProductReviewController { + + private final ProductReviewService productReviewService; + + @GetMapping("/api/products/{productId}/reviews") + public ResponseEntity getProductReviews(@PathVariable Long productId) { + List response = productReviewService.getProductReview(productId); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PostMapping("/api/products/{productId}/reviews") + public ResponseEntity registerProductReview(@PathVariable Long productId, + @RequestBody ReviewRequest reviewRequest) { + Long reviewId = productReviewService.registerProductReview(productId, + reviewRequest); + return new ResponseEntity<>(reviewId, HttpStatus.CREATED); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java b/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java deleted file mode 100644 index b068c888..00000000 --- a/src/main/java/poomasi/domain/review/dto/ProductReviewRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package poomasi.domain.review.dto; - -import poomasi.domain.product.entity.Product; -import poomasi.domain.review.entity.ProductReview; - -public record ProductReviewRequest( - float rating, - String content -) { - - public ProductReview toEntity(Product product) { - return new ProductReview(this.rating, this.content, product); - } -} diff --git a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java deleted file mode 100644 index e3a40eb2..00000000 --- a/src/main/java/poomasi/domain/review/dto/ProductReviewResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package poomasi.domain.review.dto; - -import java.util.List; -import poomasi.domain.review.entity.ProductReview; -import poomasi.domain.review.entity.ProductReviewPhoto; - -public record ProductReviewResponse - (long id, - long productId, - //long reviewerId, - float rating, - String content, - List imageUrls - ) { - - public static ProductReviewResponse fromEntity(ProductReview productReview) { - return new ProductReviewResponse( - productReview.getId(), - productReview.getProduct().getId(), - //productReview.getReviewer().getId(), - productReview.getRating(), - productReview.getContent(), - productReview.getImageUrl().stream().map(ProductReviewPhoto::getUrl).toList() - ); - } -} diff --git a/src/main/java/poomasi/domain/review/dto/ReviewRequest.java b/src/main/java/poomasi/domain/review/dto/ReviewRequest.java new file mode 100644 index 00000000..0e61fa12 --- /dev/null +++ b/src/main/java/poomasi/domain/review/dto/ReviewRequest.java @@ -0,0 +1,13 @@ +package poomasi.domain.review.dto; + +import poomasi.domain.review.entity.Review; + +public record ReviewRequest( + Float rating, + String content +) { + + public Review toEntity(Long entityId) { + return new Review(this.rating, this.content, entityId); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java new file mode 100644 index 00000000..8b25c4da --- /dev/null +++ b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java @@ -0,0 +1,24 @@ +package poomasi.domain.review.dto; + +import java.util.List; +import poomasi.domain.review.entity.Review; + +public record ReviewResponse + (Long id, + Long productId, + //Long reviewerId, + Float rating, + String content + //List imageUrls + ) { + + public static ReviewResponse fromEntity(Review review) { + return new ReviewResponse( + review.getId(), + review.getEntityId(), + //productReview.getReviewer().getId(), + review.getRating(), + review.getContent() + ); + } +} diff --git a/src/main/java/poomasi/domain/review/entity/EntityType.java b/src/main/java/poomasi/domain/review/entity/EntityType.java new file mode 100644 index 00000000..d857fabd --- /dev/null +++ b/src/main/java/poomasi/domain/review/entity/EntityType.java @@ -0,0 +1,6 @@ +package poomasi.domain.review.entity; + +public enum EntityType { + PRODUCT, + FARM +} diff --git a/src/main/java/poomasi/domain/review/entity/ProductReview.java b/src/main/java/poomasi/domain/review/entity/ProductReview.java deleted file mode 100644 index d96c013b..00000000 --- a/src/main/java/poomasi/domain/review/entity/ProductReview.java +++ /dev/null @@ -1,70 +0,0 @@ -package poomasi.domain.review.entity; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.Comment; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.product.entity.Product; -import poomasi.domain.review.dto.ProductReviewRequest; - -@Entity -@Getter -@NoArgsConstructor -public class ProductReview { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Comment("별점") - private float rating; - - @Comment("리뷰 내용") - private String content; - - @CreationTimestamp - private LocalDateTime createdAt; - - @UpdateTimestamp - private LocalDateTime updatedAt; - - @ManyToOne(fetch = FetchType.LAZY) - private Product product; - - @OneToMany(mappedBy = "productReview", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List imageUrl = new ArrayList<>(); - -// @Comment("작성자") -// @ManyToOne -// private Member reviewer; - - public ProductReview(float rating, String content, Product product) { - this.rating = rating; - this.content = content; - this.product = product; - } - - public ProductReviewPhoto addPhoto(String url) { - ProductReviewPhoto productReviewPhoto = new ProductReviewPhoto(this, url); - productReviewPhoto.setReview(this); - imageUrl.add(productReviewPhoto); - return productReviewPhoto; - } - - public void modifyReview(ProductReviewRequest productReviewRequest) { - this.rating = productReviewRequest.rating(); - this.content = productReviewRequest.content(); - } -} diff --git a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java b/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java deleted file mode 100644 index b5a498c3..00000000 --- a/src/main/java/poomasi/domain/review/entity/ProductReviewPhoto.java +++ /dev/null @@ -1,37 +0,0 @@ -package poomasi.domain.review.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.Comment; - -@Entity -@NoArgsConstructor -@Getter -public class ProductReviewPhoto { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; - - @Comment("리뷰") - @ManyToOne(fetch = FetchType.LAZY) - private ProductReview productReview; - - @Comment("사진 경로") - private String url; - - public ProductReviewPhoto(ProductReview productReview, String url) { - this.productReview = productReview; - this.url = url; - } - - public void setReview(ProductReview productReview) { - this.productReview = productReview; - } -} diff --git a/src/main/java/poomasi/domain/review/entity/Review.java b/src/main/java/poomasi/domain/review/entity/Review.java new file mode 100644 index 00000000..b72da487 --- /dev/null +++ b/src/main/java/poomasi/domain/review/entity/Review.java @@ -0,0 +1,63 @@ +package poomasi.domain.review.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.review.dto.ReviewRequest; + +@Entity +@Getter +@NoArgsConstructor +public class Review { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("별점") + private Float rating; + + @Comment("리뷰 내용") + private String content; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + @Comment("엔티티 아이디") + private Long entityId; + + @Comment("엔티티 타입") + @Enumerated(EnumType.STRING) + private EntityType entityType; + +// @Comment("작성자") +// @ManyToOne +// private Member reviewer; + + public Review(Float rating, String content, Long entityId) { + this.rating = rating; + this.content = content; + this.entityId = entityId; + } + + public void modifyReview(ReviewRequest reviewRequest) { + this.rating = reviewRequest.rating(); + this.content = reviewRequest.content(); + } + + public void setReviewType(EntityType entityType) { + this.entityType = entityType; + } +} diff --git a/src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java b/src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java deleted file mode 100644 index 36937c68..00000000 --- a/src/main/java/poomasi/domain/review/repository/ProductReviewPhotoRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package poomasi.domain.review.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import poomasi.domain.review.entity.ProductReviewPhoto; - -@Repository -public interface ProductReviewPhotoRepository extends JpaRepository { - -} diff --git a/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java deleted file mode 100644 index 4b448d15..00000000 --- a/src/main/java/poomasi/domain/review/repository/ProductReviewRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package poomasi.domain.review.repository; - -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; -import poomasi.domain.review.entity.ProductReview; - -@Repository -public interface ProductReviewRepository extends JpaRepository { - - @Query("select r from ProductReview r where r.product.id = :productId") - List findByProductId(long productId); -} diff --git a/src/main/java/poomasi/domain/review/repository/ReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java new file mode 100644 index 00000000..1457fc6f --- /dev/null +++ b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java @@ -0,0 +1,17 @@ +package poomasi.domain.review.repository; + +import java.util.Collection; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import poomasi.domain.review.entity.Review; + +@Repository +public interface ReviewRepository extends JpaRepository { + @Query("select r from Review r where r.entityId = :productId and r.entityType = 'PRODUCT'") + List findByProductId(Long productId); + + @Query("select r from Review r where r.entityId = :farmId and r.entityType = 'FARM'") + List findByFarmId(Long farmId); +} diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java b/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java deleted file mode 100644 index 557f4ded..00000000 --- a/src/main/java/poomasi/domain/review/service/ProductReviewCustomerService.java +++ /dev/null @@ -1,68 +0,0 @@ -package poomasi.domain.review.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; -import poomasi.domain.review.dto.ProductReviewRequest; -import poomasi.domain.review.entity.ProductReview; -import poomasi.domain.review.entity.ProductReviewPhoto; -import poomasi.domain.review.repository.ProductReviewPhotoRepository; -import poomasi.domain.review.repository.ProductReviewRepository; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; - -@Service -@RequiredArgsConstructor -public class ProductReviewCustomerService { - - private final ProductReviewRepository productReviewRepository; - private final ProductRepository productRepository; - private final ProductReviewPhotoRepository productReviewPhotoRepository; - - @Transactional - public long registerReview(long productId, ProductReviewRequest productReviewRequest) { - //이미지 저장하고 주소 받아와서 review에 추가해주기 - String url1 = "test1"; - String url2 = "test2"; - String url3 = "test3"; - - Product product = getProductByProductId(productId); - ProductReview pReview = productReviewRequest.toEntity(product); - pReview = productReviewRepository.save(pReview); - product.addReview(pReview); - - ProductReviewPhoto reviewPhoto1 = pReview.addPhoto(url1); - productReviewPhotoRepository.save(reviewPhoto1); - ProductReviewPhoto reviewPhoto2 = pReview.addPhoto(url2); - productReviewPhotoRepository.save(reviewPhoto2); - ProductReviewPhoto reviewPhoto3 = pReview.addPhoto(url3); - productReviewPhotoRepository.save(reviewPhoto3); - - return pReview.getId(); - } - - - private Product getProductByProductId(long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); - } - - @Transactional - public void deleteReview(long reviewId) { - ProductReview review = getReviewById(reviewId); - productReviewRepository.delete(review); - } - - private ProductReview getReviewById(long reviewId) { - return productReviewRepository.findById(reviewId) - .orElseThrow(() -> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); - } - - @Transactional - public void modifyReview(long reviewId, ProductReviewRequest productReviewRequest) { - ProductReview pReview = getReviewById(reviewId); - pReview.modifyReview(productReviewRequest); - } -} diff --git a/src/main/java/poomasi/domain/review/service/ProductReviewService.java b/src/main/java/poomasi/domain/review/service/ProductReviewService.java deleted file mode 100644 index f2792bb7..00000000 --- a/src/main/java/poomasi/domain/review/service/ProductReviewService.java +++ /dev/null @@ -1,33 +0,0 @@ -package poomasi.domain.review.service; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; -import poomasi.domain.review.dto.ProductReviewResponse; -import poomasi.domain.review.repository.ProductReviewRepository; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; - -@Service -@RequiredArgsConstructor -public class ProductReviewService { - - private final ProductReviewRepository productReviewRepository; - private final ProductRepository productRepository; - - public List getProductReview(long productId) { - getProductByProductId(productId); //상품이 존재하는지 체크 - - return productReviewRepository.findByProductId(productId).stream() - .map(ProductReviewResponse::fromEntity).toList(); - } - - private Product getProductByProductId(long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); - } - - -} diff --git a/src/main/java/poomasi/domain/review/service/ReviewService.java b/src/main/java/poomasi/domain/review/service/ReviewService.java new file mode 100644 index 00000000..7462969a --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/ReviewService.java @@ -0,0 +1,33 @@ +package poomasi.domain.review.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.entity.Review; +import poomasi.domain.review.repository.ReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ReviewService { + private final ReviewRepository reviewRepository; + + @Transactional + public void modifyReview(Long reviewId, ReviewRequest reviewRequest) { + Review pReview = getReviewById(reviewId); + pReview.modifyReview(reviewRequest); + } + + @Transactional + public void deleteReview(Long reviewId) { + Review review = getReviewById(reviewId); + reviewRepository.delete(review); + } + + private Review getReviewById(Long reviewId) { + return reviewRepository.findById(reviewId) + .orElseThrow(() -> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); + } +} diff --git a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java new file mode 100644 index 00000000..648765d7 --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java @@ -0,0 +1,47 @@ +package poomasi.domain.review.service.farm; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.repository.FarmRepository; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.entity.EntityType; +import poomasi.domain.review.entity.Review; +import poomasi.domain.review.repository.ReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class FarmReviewService { + private final ReviewRepository reviewRepository; + private final FarmRepository farmRepository; + + public List getFarmReview(Long farmId) { + getFarmByFarmId(farmId); //상품이 존재하는지 체크 + + return reviewRepository.findByFarmId(farmId).stream() + .map(ReviewResponse::fromEntity).toList(); + } + + @Transactional + public Long registerFarmReview(Long entityId, ReviewRequest reviewRequest) { + // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 + + Review pReview = reviewRequest.toEntity(entityId); + pReview.setReviewType(EntityType.FARM); + pReview = reviewRepository.save(pReview); + + return pReview.getId(); + } + + private Farm getFarmByFarmId(Long farmId) { + return farmRepository.findById(farmId) + .orElseThrow(() -> new BusinessException(BusinessError.FARM_NOT_FOUND)); + } +} diff --git a/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java new file mode 100644 index 00000000..cadec05c --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java @@ -0,0 +1,47 @@ +package poomasi.domain.review.service.product; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.entity.EntityType; +import poomasi.domain.review.entity.Review; +import poomasi.domain.review.repository.ReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ProductReviewService { + + private final ReviewRepository reviewRepository; + private final ProductRepository productRepository; + + public List getProductReview(Long productId) { + getProductByProductId(productId); //상품이 존재하는지 체크 + + return reviewRepository.findByProductId(productId).stream() + .map(ReviewResponse::fromEntity).toList(); + } + + @Transactional + public Long registerProductReview(Long entityId, ReviewRequest reviewRequest) { + // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 + + Review pReview = reviewRequest.toEntity(entityId); + pReview.setReviewType(EntityType.PRODUCT); + pReview = reviewRepository.save(pReview); + + return pReview.getId(); + } + + private Product getProductByProductId(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + } + +} From f0b0d71715563e2619ad50bd445072346cacdc6d Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Mon, 7 Oct 2024 22:04:41 +0900 Subject: [PATCH 11/17] =?UTF-8?q?style:=20=EC=95=84=20=EB=A7=9E=EB=8B=A4?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CategoryAdminController.java | 7 +++- .../controller/CategoryController.java | 1 - .../product/_category/entity/Category.java | 3 +- .../service/CategoryAdminService.java | 1 + .../_category/service/CategoryService.java | 1 - .../product/controller/ProductController.java | 11 ++++--- .../controller/ProductFarmerController.java | 13 ++++++-- .../domain/product/dto/ProductResponse.java | 3 +- .../dto/UpdateProductQuantityRequest.java | 1 + .../domain/product/entity/Product.java | 32 +++++++++++-------- .../product/repository/ProductRepository.java | 7 ++-- .../product/service/ProductFarmerService.java | 5 +-- .../product/service/ProductService.java | 6 ++-- .../review/controller/ReviewController.java | 5 ++- .../controller/farm/FarmReviewController.java | 2 +- .../domain/review/dto/ReviewResponse.java | 1 - .../review/repository/ReviewRepository.java | 2 +- .../domain/review/service/ReviewService.java | 1 + .../service/farm/FarmReviewService.java | 3 +- 19 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java index 3b755741..46bd6e05 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java @@ -3,7 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; import poomasi.domain.product._category.dto.CategoryRequest; import poomasi.domain.product._category.service.CategoryAdminService; diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java index 4c7730fa..1979cd47 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java @@ -1,7 +1,6 @@ package poomasi.domain.product._category.controller; import java.util.List; - import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/poomasi/domain/product/_category/entity/Category.java b/src/main/java/poomasi/domain/product/_category/entity/Category.java index e256f0c7..b1957cdb 100644 --- a/src/main/java/poomasi/domain/product/_category/entity/Category.java +++ b/src/main/java/poomasi/domain/product/_category/entity/Category.java @@ -7,10 +7,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; - import java.util.ArrayList; import java.util.List; - import lombok.Getter; import lombok.NoArgsConstructor; import poomasi.domain.product._category.dto.CategoryRequest; @@ -20,6 +18,7 @@ @Getter @NoArgsConstructor public class Category { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java index b2e12c64..f99c1c46 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java @@ -12,6 +12,7 @@ @Service @RequiredArgsConstructor public class CategoryAdminService { + private final CategoryRepository categoryRepository; public Long registerCategory(CategoryRequest categoryRequest) { diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java index 18ea8731..51594960 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java @@ -1,7 +1,6 @@ package poomasi.domain.product._category.service; import java.util.List; - import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product._category.dto.CategoryResponse; diff --git a/src/main/java/poomasi/domain/product/controller/ProductController.java b/src/main/java/poomasi/domain/product/controller/ProductController.java index 0d78dbc9..5d9b4444 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductController.java @@ -1,18 +1,21 @@ package poomasi.domain.product.controller; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.product.service.ProductService; +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 poomasi.domain.product.dto.ProductResponse; - -import java.util.List; +import poomasi.domain.product.service.ProductService; @RestController @RequiredArgsConstructor @RequestMapping("/api/product") public class ProductController { + private final ProductService productService; @GetMapping("") diff --git a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java index cdfbb7dd..cb97f6a1 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java @@ -4,7 +4,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.service.ProductFarmerService; @@ -25,7 +32,7 @@ public ResponseEntity registerProduct(@RequestBody ProductRegisterRequest pro @PutMapping("/{productId}") public ResponseEntity modifyProduct(@RequestBody ProductRegisterRequest product, - @PathVariable Long productId) { + @PathVariable Long productId) { productFarmerService.modifyProduct(product, productId); return new ResponseEntity<>(productId, HttpStatus.OK); } @@ -41,7 +48,7 @@ public ResponseEntity deleteProduct(@PathVariable Long productId) { @PatchMapping("/{productId}") public ResponseEntity updateProductQuantity(@PathVariable Long productId, - @RequestBody UpdateProductQuantityRequest request) { + @RequestBody UpdateProductQuantityRequest request) { log.debug("Product ID: {}", productId); log.debug("Update Request: {}", request); productFarmerService.addQuantity(productId, request); diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 1745b503..89eca75c 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -1,9 +1,7 @@ package poomasi.domain.product.dto; -import java.util.List; import lombok.Builder; import poomasi.domain.product.entity.Product; -import poomasi.domain.review.dto.ReviewResponse; @Builder public record ProductResponse( @@ -15,6 +13,7 @@ public record ProductResponse( String imageUrl, Long categoryId ) { + public static ProductResponse fromEntity(Product product) { return ProductResponse.builder() .id(product.getId()) diff --git a/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java b/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java index 0a082c87..bff0b8b7 100644 --- a/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java +++ b/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java @@ -1,5 +1,6 @@ package poomasi.domain.product.dto; public record UpdateProductQuantityRequest(Integer quantity) { + } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index e4db9e1b..f39e5cb0 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,6 +1,15 @@ package poomasi.domain.product.entity; -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,15 +20,12 @@ import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.Review; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - @Entity @Getter @NoArgsConstructor @SQLDelete(sql = "UPDATE product SET deleted_at = current_timestamp WHERE id = ?") public class Product { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -63,13 +69,13 @@ public class Product { @Builder public Product(Long productId, - Long categoryId, - Long farmerId, - String name, - String description, - String imageUrl, - Integer stock, - Long price) { + Long categoryId, + Long farmerId, + String name, + String description, + String imageUrl, + Integer stock, + Long price) { this.categoryId = categoryId; this.farmerId = farmerId; this.name = name; @@ -89,7 +95,7 @@ public Product modify(ProductRegisterRequest productRegisterRequest) { return this; } - public void addStock (Integer stock) { + public void addStock(Integer stock) { this.stock += stock; } diff --git a/src/main/java/poomasi/domain/product/repository/ProductRepository.java b/src/main/java/poomasi/domain/product/repository/ProductRepository.java index 20f7323a..17ce4ed0 100644 --- a/src/main/java/poomasi/domain/product/repository/ProductRepository.java +++ b/src/main/java/poomasi/domain/product/repository/ProductRepository.java @@ -1,15 +1,16 @@ package poomasi.domain.product.repository; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import poomasi.domain.product.entity.Product; -import java.util.List; -import java.util.Optional; - @Repository public interface ProductRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(Long id); + List findAllByDeletedAtIsNull(); } diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index 43c3091a..b37d4605 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -69,7 +69,8 @@ private Product getProductByProductId(Long productId) { .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } - private Category getCategory(Long categoryId){ - return categoryRepository.findById(categoryId).orElseThrow(()->new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + private Category getCategory(Long categoryId) { + return categoryRepository.findById(categoryId) + .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); } } diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java index c25b99e9..765a18b5 100644 --- a/src/main/java/poomasi/domain/product/service/ProductService.java +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -1,17 +1,17 @@ package poomasi.domain.product.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import poomasi.domain.product.repository.ProductRepository; import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.product.repository.ProductRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; -import java.util.List; - @Service @RequiredArgsConstructor public class ProductService { + private final ProductRepository productRepository; public List getAllProducts() { diff --git a/src/main/java/poomasi/domain/review/controller/ReviewController.java b/src/main/java/poomasi/domain/review/controller/ReviewController.java index dae1b3da..a9c2f580 100644 --- a/src/main/java/poomasi/domain/review/controller/ReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/ReviewController.java @@ -14,7 +14,9 @@ @Controller @RequiredArgsConstructor public class ReviewController { + private final ReviewService reviewService; + @DeleteMapping("/api/reviews/{reviewId}") public ResponseEntity deleteProductReview(@PathVariable Long reviewId) { reviewService.deleteReview(reviewId); @@ -22,7 +24,8 @@ public ResponseEntity deleteProductReview(@PathVariable Long reviewId) { } @PutMapping("/api/reviews/{reviewId}") - public ResponseEntity modifyProductReview(@PathVariable Long reviewId, @RequestBody ReviewRequest reviewRequest){ + public ResponseEntity modifyProductReview(@PathVariable Long reviewId, + @RequestBody ReviewRequest reviewRequest) { reviewService.modifyReview(reviewId, reviewRequest); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java index 83e3f79f..8c78705a 100644 --- a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java +++ b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java @@ -11,12 +11,12 @@ import org.springframework.web.bind.annotation.RequestBody; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; -import poomasi.domain.review.entity.EntityType; import poomasi.domain.review.service.farm.FarmReviewService; @Controller @RequiredArgsConstructor public class FarmReviewController { + private final FarmReviewService farmReviewService; @GetMapping("/api/farm/{farmId}/reviews") diff --git a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java index 8b25c4da..dfd362b2 100644 --- a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java +++ b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java @@ -1,6 +1,5 @@ package poomasi.domain.review.dto; -import java.util.List; import poomasi.domain.review.entity.Review; public record ReviewResponse diff --git a/src/main/java/poomasi/domain/review/repository/ReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java index 1457fc6f..6a506d4f 100644 --- a/src/main/java/poomasi/domain/review/repository/ReviewRepository.java +++ b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java @@ -1,6 +1,5 @@ package poomasi.domain.review.repository; -import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -9,6 +8,7 @@ @Repository public interface ReviewRepository extends JpaRepository { + @Query("select r from Review r where r.entityId = :productId and r.entityType = 'PRODUCT'") List findByProductId(Long productId); diff --git a/src/main/java/poomasi/domain/review/service/ReviewService.java b/src/main/java/poomasi/domain/review/service/ReviewService.java index 7462969a..3115163c 100644 --- a/src/main/java/poomasi/domain/review/service/ReviewService.java +++ b/src/main/java/poomasi/domain/review/service/ReviewService.java @@ -12,6 +12,7 @@ @Service @RequiredArgsConstructor public class ReviewService { + private final ReviewRepository reviewRepository; @Transactional diff --git a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java index 648765d7..131668a3 100644 --- a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java +++ b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java @@ -6,8 +6,6 @@ import org.springframework.transaction.annotation.Transactional; import poomasi.domain.farm.entity.Farm; import poomasi.domain.farm.repository.FarmRepository; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; import poomasi.domain.review.dto.ReviewRequest; import poomasi.domain.review.dto.ReviewResponse; import poomasi.domain.review.entity.EntityType; @@ -19,6 +17,7 @@ @Service @RequiredArgsConstructor public class FarmReviewService { + private final ReviewRepository reviewRepository; private final FarmRepository farmRepository; From d178a5567338d6f054eeca0d85de76072ec8f3ae Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Mon, 7 Oct 2024 22:04:41 +0900 Subject: [PATCH 12/17] =?UTF-8?q?B=20style:=20=EC=95=84=20=EB=A7=9E?= =?UTF-8?q?=EB=8B=A4=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CategoryAdminController.java | 7 ++- .../controller/CategoryController.java | 11 ++++- .../product/_category/entity/Category.java | 21 ++++++++- .../service/CategoryAdminService.java | 1 + .../_category/service/CategoryService.java | 1 - .../product/controller/ProductController.java | 11 +++-- .../controller/ProductFarmerController.java | 13 ++++-- .../domain/product/dto/ProductResponse.java | 1 + .../dto/UpdateProductQuantityRequest.java | 1 + .../domain/product/entity/Product.java | 42 ++++++++++++----- .../product/repository/ProductRepository.java | 7 +-- .../product/service/ProductFarmerService.java | 35 ++++++++++---- .../product/service/ProductService.java | 6 +-- .../review/controller/ReviewController.java | 32 +++++++++++++ .../controller/farm/FarmReviewController.java | 35 ++++++++++++++ .../domain/review/dto/ReviewResponse.java | 23 ++++++++++ .../review/repository/ReviewRepository.java | 17 +++++++ .../domain/review/service/ReviewService.java | 34 ++++++++++++++ .../service/farm/FarmReviewService.java | 46 +++++++++++++++++++ 19 files changed, 308 insertions(+), 36 deletions(-) create mode 100644 src/main/java/poomasi/domain/review/controller/ReviewController.java create mode 100644 src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java create mode 100644 src/main/java/poomasi/domain/review/dto/ReviewResponse.java create mode 100644 src/main/java/poomasi/domain/review/repository/ReviewRepository.java create mode 100644 src/main/java/poomasi/domain/review/service/ReviewService.java create mode 100644 src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java index 3b755741..46bd6e05 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryAdminController.java @@ -3,7 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; import poomasi.domain.product._category.dto.CategoryRequest; import poomasi.domain.product._category.service.CategoryAdminService; diff --git a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java index 68847605..1979cd47 100644 --- a/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java +++ b/src/main/java/poomasi/domain/product/_category/controller/CategoryController.java @@ -1,14 +1,16 @@ package poomasi.domain.product._category.controller; +import java.util.List; 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.PathVariable; import org.springframework.web.bind.annotation.RestController; import poomasi.domain.product._category.dto.CategoryResponse; +import poomasi.domain.product._category.dto.ProductListInCategoryResponse; import poomasi.domain.product._category.service.CategoryService; -import java.util.List; @RestController @RequiredArgsConstructor public class CategoryController { @@ -20,4 +22,11 @@ public ResponseEntity getAllCategories() { List categories = categoryService.getAllCategories(); return new ResponseEntity<>(categories, HttpStatus.OK); } + + @GetMapping("/api/categories/{categoryId}") + public ResponseEntity getCategoryById(@PathVariable Long categoryId) { + List productList = categoryService.getProductInCategory( + categoryId); + return new ResponseEntity<>(productList, HttpStatus.OK); + } } diff --git a/src/main/java/poomasi/domain/product/_category/entity/Category.java b/src/main/java/poomasi/domain/product/_category/entity/Category.java index dd855459..b1957cdb 100644 --- a/src/main/java/poomasi/domain/product/_category/entity/Category.java +++ b/src/main/java/poomasi/domain/product/_category/entity/Category.java @@ -1,22 +1,33 @@ package poomasi.domain.product._category.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; import poomasi.domain.product._category.dto.CategoryRequest; +import poomasi.domain.product.entity.Product; @Entity @Getter @NoArgsConstructor public class Category { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + private Long id; private String name; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "categoryId") + List products = new ArrayList<>(); + public Category(String name) { this.name = name; } @@ -24,4 +35,12 @@ public Category(String name) { public void modifyName(CategoryRequest categoryRequest) { this.name = categoryRequest.name(); } + + public void deleteProduct(Product product) { + this.products.remove(product); + } + + public void addProduct(Product saveProduct) { + this.products.add(saveProduct); + } } diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java index b2e12c64..f99c1c46 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryAdminService.java @@ -12,6 +12,7 @@ @Service @RequiredArgsConstructor public class CategoryAdminService { + private final CategoryRepository categoryRepository; public Long registerCategory(CategoryRequest categoryRequest) { diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java index f6b5dfce..b96b90ee 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java @@ -1,7 +1,6 @@ package poomasi.domain.product._category.service; import java.util.List; - import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product._category.dto.CategoryResponse; diff --git a/src/main/java/poomasi/domain/product/controller/ProductController.java b/src/main/java/poomasi/domain/product/controller/ProductController.java index 0d78dbc9..5d9b4444 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductController.java @@ -1,18 +1,21 @@ package poomasi.domain.product.controller; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.product.service.ProductService; +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 poomasi.domain.product.dto.ProductResponse; - -import java.util.List; +import poomasi.domain.product.service.ProductService; @RestController @RequiredArgsConstructor @RequestMapping("/api/product") public class ProductController { + private final ProductService productService; @GetMapping("") diff --git a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java index ba15df19..70c3f4b2 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java @@ -4,7 +4,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.service.ProductFarmerService; @@ -25,7 +32,7 @@ public ResponseEntity registerProduct(@RequestBody ProductRegisterRequest pro @PutMapping("/{productId}") public ResponseEntity modifyProduct(@RequestBody ProductRegisterRequest product, - @PathVariable Long productId) { + @PathVariable Long productId) { productFarmerService.modifyProduct(product, productId); return new ResponseEntity<>(productId, HttpStatus.OK); } @@ -41,7 +48,7 @@ public ResponseEntity deleteProduct(@PathVariable Long productId) { @PatchMapping("/{productId}") public ResponseEntity updateProductQuantity(@PathVariable Long productId, - @RequestBody UpdateProductQuantityRequest request) { + @RequestBody UpdateProductQuantityRequest request) { log.debug("Product ID: {}", productId); log.debug("Update Request: {}", request); productFarmerService.addQuantity(productId, request); diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 75db2432..4264d2bf 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -13,6 +13,7 @@ public record ProductResponse( String imageUrl, long categoryId ) { + public static ProductResponse fromEntity(Product product) { return ProductResponse.builder() .id(product.getId()) diff --git a/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java b/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java index 0a082c87..bff0b8b7 100644 --- a/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java +++ b/src/main/java/poomasi/domain/product/dto/UpdateProductQuantityRequest.java @@ -1,5 +1,6 @@ package poomasi.domain.product.dto; public record UpdateProductQuantityRequest(Integer quantity) { + } diff --git a/src/main/java/poomasi/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index c6d2a6cd..f39e5cb0 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,9 +1,15 @@ package poomasi.domain.product.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,14 +18,14 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.product.dto.ProductRegisterRequest; - -import java.time.LocalDateTime; +import poomasi.domain.review.entity.Review; @Entity @Getter @NoArgsConstructor @SQLDelete(sql = "UPDATE product SET deleted_at = current_timestamp WHERE id = ?") public class Product { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -40,7 +46,7 @@ public class Product { private String imageUrl; @Comment("재고") - private int stock; + private Integer stock; @Comment("가격") private Long price; @@ -54,16 +60,22 @@ public class Product { @UpdateTimestamp private LocalDateTime updatedAt; + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) + @JoinColumn(name = "entityId") + List reviewList = new ArrayList<>(); + + @Comment("평균 평점") + private double averageRating = 0.0; @Builder public Product(Long productId, - Long categoryId, - Long farmerId, //등록한 사람 - String name, - String description, - String imageUrl, - int stock, - Long price) { + Long categoryId, + Long farmerId, + String name, + String description, + String imageUrl, + Integer stock, + Long price) { this.categoryId = categoryId; this.farmerId = farmerId; this.name = name; @@ -83,8 +95,16 @@ public Product modify(ProductRegisterRequest productRegisterRequest) { return this; } - public void addQuantity(int stock) { + public void addStock(Integer stock) { this.stock += stock; } + public void addReview(Review pReview) { + this.reviewList.add(pReview); + this.averageRating = reviewList.stream() + .mapToDouble(Review::getRating) // 각 리뷰의 평점을 double로 변환 + .average() // 평균 계산 + .orElse(0.0); + } + } diff --git a/src/main/java/poomasi/domain/product/repository/ProductRepository.java b/src/main/java/poomasi/domain/product/repository/ProductRepository.java index 20f7323a..17ce4ed0 100644 --- a/src/main/java/poomasi/domain/product/repository/ProductRepository.java +++ b/src/main/java/poomasi/domain/product/repository/ProductRepository.java @@ -1,15 +1,16 @@ package poomasi.domain.product.repository; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import poomasi.domain.product.entity.Product; -import java.util.List; -import java.util.Optional; - @Repository public interface ProductRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(Long id); + List findAllByDeletedAtIsNull(); } diff --git a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java index dd4dfa67..b37d4605 100644 --- a/src/main/java/poomasi/domain/product/service/ProductFarmerService.java +++ b/src/main/java/poomasi/domain/product/service/ProductFarmerService.java @@ -3,8 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.member.service.MemberService; -import poomasi.domain.product._category.service.CategoryService; +import poomasi.domain.product._category.entity.Category; +import poomasi.domain.product._category.repository.CategoryRepository; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.product.dto.UpdateProductQuantityRequest; import poomasi.domain.product.entity.Product; @@ -17,34 +17,49 @@ public class ProductFarmerService { private final ProductRepository productRepository; - private final CategoryService categoryService; - private final MemberService memberService; + private final CategoryRepository categoryRepository; + //private final MemberService memberService; public Long registerProduct(ProductRegisterRequest request) { - memberService.isFarmer(request.farmerId()); - categoryService.getCategory(request.categoryId()); + //memberService.isFarmer(request.farmerId()); + Category category = getCategory(request.categoryId()); Product saveProduct = productRepository.save(request.toEntity()); + category.addProduct(saveProduct); return saveProduct.getId(); } + + @Transactional public void modifyProduct(ProductRegisterRequest productRequest, Long productId) { // TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); - productRepository.save(product.modify(productRequest)); + Long categoryId = product.getCategoryId(); + Category oldCategory = getCategory(categoryId); + + oldCategory.deleteProduct(product);//원래 카테고리에서 삭제 + product = productRepository.save(product.modify(productRequest)); //상품 갱신 + + categoryId = productRequest.categoryId(); + Category newCategory = getCategory(categoryId); + newCategory.addProduct(product); } @Transactional public void deleteProduct(Long productId) { //TODO: 주인인지 알아보기 Product product = getProductByProductId(productId); + Long categoryId = product.getCategoryId(); + Category category = getCategory(categoryId); + + category.deleteProduct(product); productRepository.delete(product); } @Transactional public void addQuantity(Long productId, UpdateProductQuantityRequest request) { Product productByProductId = getProductByProductId(productId); - productByProductId.addQuantity(request.quantity()); + productByProductId.addStock(request.quantity()); productRepository.save(productByProductId); } @@ -54,4 +69,8 @@ private Product getProductByProductId(Long productId) { .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } + private Category getCategory(Long categoryId) { + return categoryRepository.findById(categoryId) + .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); + } } diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java index c25b99e9..765a18b5 100644 --- a/src/main/java/poomasi/domain/product/service/ProductService.java +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -1,17 +1,17 @@ package poomasi.domain.product.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import poomasi.domain.product.repository.ProductRepository; import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.product.repository.ProductRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; -import java.util.List; - @Service @RequiredArgsConstructor public class ProductService { + private final ProductRepository productRepository; public List getAllProducts() { diff --git a/src/main/java/poomasi/domain/review/controller/ReviewController.java b/src/main/java/poomasi/domain/review/controller/ReviewController.java new file mode 100644 index 00000000..a9c2f580 --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/ReviewController.java @@ -0,0 +1,32 @@ +package poomasi.domain.review.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.service.ReviewService; + +@Controller +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @DeleteMapping("/api/reviews/{reviewId}") + public ResponseEntity deleteProductReview(@PathVariable Long reviewId) { + reviewService.deleteReview(reviewId); + return new ResponseEntity<>(HttpStatus.OK); + } + + @PutMapping("/api/reviews/{reviewId}") + public ResponseEntity modifyProductReview(@PathVariable Long reviewId, + @RequestBody ReviewRequest reviewRequest) { + reviewService.modifyReview(reviewId, reviewRequest); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java new file mode 100644 index 00000000..8c78705a --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/farm/FarmReviewController.java @@ -0,0 +1,35 @@ +package poomasi.domain.review.controller.farm; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +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.RequestBody; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.service.farm.FarmReviewService; + +@Controller +@RequiredArgsConstructor +public class FarmReviewController { + + private final FarmReviewService farmReviewService; + + @GetMapping("/api/farm/{farmId}/reviews") + public ResponseEntity getProductReviews(@PathVariable Long farmId) { + List response = farmReviewService.getFarmReview(farmId); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PostMapping("/api/farm/{farmId}/reviews") + public ResponseEntity registerProductReview(@PathVariable Long farmId, + @RequestBody ReviewRequest reviewRequest) { + Long reviewId = farmReviewService.registerFarmReview(farmId, + reviewRequest); + return new ResponseEntity<>(reviewId, HttpStatus.CREATED); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ReviewResponse.java b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java new file mode 100644 index 00000000..dfd362b2 --- /dev/null +++ b/src/main/java/poomasi/domain/review/dto/ReviewResponse.java @@ -0,0 +1,23 @@ +package poomasi.domain.review.dto; + +import poomasi.domain.review.entity.Review; + +public record ReviewResponse + (Long id, + Long productId, + //Long reviewerId, + Float rating, + String content + //List imageUrls + ) { + + public static ReviewResponse fromEntity(Review review) { + return new ReviewResponse( + review.getId(), + review.getEntityId(), + //productReview.getReviewer().getId(), + review.getRating(), + review.getContent() + ); + } +} diff --git a/src/main/java/poomasi/domain/review/repository/ReviewRepository.java b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java new file mode 100644 index 00000000..6a506d4f --- /dev/null +++ b/src/main/java/poomasi/domain/review/repository/ReviewRepository.java @@ -0,0 +1,17 @@ +package poomasi.domain.review.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import poomasi.domain.review.entity.Review; + +@Repository +public interface ReviewRepository extends JpaRepository { + + @Query("select r from Review r where r.entityId = :productId and r.entityType = 'PRODUCT'") + List findByProductId(Long productId); + + @Query("select r from Review r where r.entityId = :farmId and r.entityType = 'FARM'") + List findByFarmId(Long farmId); +} diff --git a/src/main/java/poomasi/domain/review/service/ReviewService.java b/src/main/java/poomasi/domain/review/service/ReviewService.java new file mode 100644 index 00000000..3115163c --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/ReviewService.java @@ -0,0 +1,34 @@ +package poomasi.domain.review.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.entity.Review; +import poomasi.domain.review.repository.ReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + + @Transactional + public void modifyReview(Long reviewId, ReviewRequest reviewRequest) { + Review pReview = getReviewById(reviewId); + pReview.modifyReview(reviewRequest); + } + + @Transactional + public void deleteReview(Long reviewId) { + Review review = getReviewById(reviewId); + reviewRepository.delete(review); + } + + private Review getReviewById(Long reviewId) { + return reviewRepository.findById(reviewId) + .orElseThrow(() -> new BusinessException(BusinessError.REVIEW_NOT_FOUND)); + } +} diff --git a/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java new file mode 100644 index 00000000..131668a3 --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/farm/FarmReviewService.java @@ -0,0 +1,46 @@ +package poomasi.domain.review.service.farm; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.repository.FarmRepository; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.entity.EntityType; +import poomasi.domain.review.entity.Review; +import poomasi.domain.review.repository.ReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class FarmReviewService { + + private final ReviewRepository reviewRepository; + private final FarmRepository farmRepository; + + public List getFarmReview(Long farmId) { + getFarmByFarmId(farmId); //상품이 존재하는지 체크 + + return reviewRepository.findByFarmId(farmId).stream() + .map(ReviewResponse::fromEntity).toList(); + } + + @Transactional + public Long registerFarmReview(Long entityId, ReviewRequest reviewRequest) { + // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 + + Review pReview = reviewRequest.toEntity(entityId); + pReview.setReviewType(EntityType.FARM); + pReview = reviewRepository.save(pReview); + + return pReview.getId(); + } + + private Farm getFarmByFarmId(Long farmId) { + return farmRepository.findById(farmId) + .orElseThrow(() -> new BusinessException(BusinessError.FARM_NOT_FOUND)); + } +} From 57a4f64d7f955c4647158daf325666901c01d40b Mon Sep 17 00:00:00 2001 From: canyos <4581974@naver.com> Date: Mon, 7 Oct 2024 22:57:44 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20review=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_category/dto/CategoryRequest.java | 5 +- .../_category/dto/CategoryResponse.java | 10 ++- .../dto/ProductListInCategoryResponse.java | 26 ++++++++ .../_category/service/CategoryService.java | 12 +++- .../controller/ProductAdminController.java | 24 ------- .../controller/ProductFarmerController.java | 2 - .../product/dto/ProductListResponse.java | 5 -- .../product/dto/ProductRegisterRequest.java | 3 +- .../domain/product/dto/ProductResponse.java | 6 +- .../product/service/ProductAdminService.java | 22 ------- .../product/ProductReviewController.java | 35 +++++++++++ .../domain/review/dto/ReviewRequest.java | 13 ++++ .../domain/review/entity/EntityType.java | 6 ++ .../poomasi/domain/review/entity/Review.java | 63 +++++++++++++++++++ .../service/product/ProductReviewService.java | 47 ++++++++++++++ 15 files changed, 218 insertions(+), 61 deletions(-) create mode 100644 src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java delete mode 100644 src/main/java/poomasi/domain/product/controller/ProductAdminController.java delete mode 100644 src/main/java/poomasi/domain/product/dto/ProductListResponse.java delete mode 100644 src/main/java/poomasi/domain/product/service/ProductAdminService.java create mode 100644 src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java create mode 100644 src/main/java/poomasi/domain/review/dto/ReviewRequest.java create mode 100644 src/main/java/poomasi/domain/review/entity/EntityType.java create mode 100644 src/main/java/poomasi/domain/review/entity/Review.java create mode 100644 src/main/java/poomasi/domain/review/service/product/ProductReviewService.java diff --git a/src/main/java/poomasi/domain/product/_category/dto/CategoryRequest.java b/src/main/java/poomasi/domain/product/_category/dto/CategoryRequest.java index d97801f9..5fc45c31 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/CategoryRequest.java +++ b/src/main/java/poomasi/domain/product/_category/dto/CategoryRequest.java @@ -2,7 +2,10 @@ import poomasi.domain.product._category.entity.Category; -public record CategoryRequest(String name) { +public record CategoryRequest( + String name +) { + public Category toEntity() { return new Category(name); } diff --git a/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java index 5e170675..efaadcb8 100644 --- a/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java +++ b/src/main/java/poomasi/domain/product/_category/dto/CategoryResponse.java @@ -1,9 +1,15 @@ package poomasi.domain.product._category.dto; +import lombok.Builder; import poomasi.domain.product._category.entity.Category; -public record CategoryResponse(long id, String name) { +@Builder +public record CategoryResponse(Long id, String name) { + public static CategoryResponse fromEntity(Category category) { - return new CategoryResponse(category.getId(), category.getName()); + return CategoryResponse.builder() + .id(category.getId()) + .name(category.getName()) + .build(); } } diff --git a/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java new file mode 100644 index 00000000..12eff02f --- /dev/null +++ b/src/main/java/poomasi/domain/product/_category/dto/ProductListInCategoryResponse.java @@ -0,0 +1,26 @@ +package poomasi.domain.product._category.dto; + +import lombok.Builder; +import poomasi.domain.product.entity.Product; + +@Builder +public record ProductListInCategoryResponse( + Long categoryId, + String name, + String description, + String imageUrl, + Integer quantity, + Long price +) { + + public static ProductListInCategoryResponse fromEntity(Product product) { + return ProductListInCategoryResponse.builder() + .categoryId(product.getCategoryId()) + .name(product.getName()) + .description(product.getDescription()) + .imageUrl(product.getImageUrl()) + .quantity(product.getStock()) + .price(product.getPrice()) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java index b96b90ee..51594960 100644 --- a/src/main/java/poomasi/domain/product/_category/service/CategoryService.java +++ b/src/main/java/poomasi/domain/product/_category/service/CategoryService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product._category.dto.CategoryResponse; +import poomasi.domain.product._category.dto.ProductListInCategoryResponse; import poomasi.domain.product._category.entity.Category; import poomasi.domain.product._category.repository.CategoryRepository; import poomasi.global.error.BusinessError; @@ -22,9 +23,18 @@ public List getAllCategories() { .toList(); } + public List getProductInCategory(Long categoryId) { + Category category = getCategory(categoryId); + return category.getProducts() + .stream() + .map(ProductListInCategoryResponse::fromEntity) + .toList(); + } + public Category getCategory(Long categoryId) { - return categoryRepository.findById(categoryId) + return categoryRepository.findById(categoryId) .orElseThrow(() -> new BusinessException(BusinessError.CATEGORY_NOT_FOUND)); } + } diff --git a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java b/src/main/java/poomasi/domain/product/controller/ProductAdminController.java deleted file mode 100644 index fe0b5380..00000000 --- a/src/main/java/poomasi/domain/product/controller/ProductAdminController.java +++ /dev/null @@ -1,24 +0,0 @@ -package poomasi.domain.product.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import poomasi.domain.product.service.ProductAdminService; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/product") -public class ProductAdminController { - - private final ProductAdminService productAdminService; - - @PutMapping("/{productId}/open") - ResponseEntity openProduct(@PathVariable Long productId) { - productAdminService.openProduct(productId); - return new ResponseEntity<>(productId, HttpStatus.OK); - } -} diff --git a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java index 70c3f4b2..cb97f6a1 100644 --- a/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java +++ b/src/main/java/poomasi/domain/product/controller/ProductFarmerController.java @@ -54,6 +54,4 @@ public ResponseEntity updateProductQuantity(@PathVariable Long productId, productFarmerService.addQuantity(productId, request); return new ResponseEntity<>(HttpStatus.OK); } - - } diff --git a/src/main/java/poomasi/domain/product/dto/ProductListResponse.java b/src/main/java/poomasi/domain/product/dto/ProductListResponse.java deleted file mode 100644 index 3faeeb79..00000000 --- a/src/main/java/poomasi/domain/product/dto/ProductListResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package poomasi.domain.product.dto; - -public class ProductListResponse { - -} diff --git a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java index 95c0189d..96278af7 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java +++ b/src/main/java/poomasi/domain/product/dto/ProductRegisterRequest.java @@ -8,7 +8,7 @@ public record ProductRegisterRequest( String name, String description, String imageUrl, - int stock, + Integer stock, Long price ) { @@ -17,6 +17,7 @@ public Product toEntity() { .categoryId(categoryId) .farmerId(farmerId) .name(name) + .stock(stock) .description(description) .imageUrl(imageUrl) .stock(stock) diff --git a/src/main/java/poomasi/domain/product/dto/ProductResponse.java b/src/main/java/poomasi/domain/product/dto/ProductResponse.java index 4264d2bf..89eca75c 100644 --- a/src/main/java/poomasi/domain/product/dto/ProductResponse.java +++ b/src/main/java/poomasi/domain/product/dto/ProductResponse.java @@ -5,13 +5,13 @@ @Builder public record ProductResponse( - long id, + Long id, String name, Long price, - int stock, + Integer stock, String description, String imageUrl, - long categoryId + Long categoryId ) { public static ProductResponse fromEntity(Product product) { diff --git a/src/main/java/poomasi/domain/product/service/ProductAdminService.java b/src/main/java/poomasi/domain/product/service/ProductAdminService.java deleted file mode 100644 index e55beb8f..00000000 --- a/src/main/java/poomasi/domain/product/service/ProductAdminService.java +++ /dev/null @@ -1,22 +0,0 @@ -package poomasi.domain.product.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; - -@Service -@RequiredArgsConstructor -public class ProductAdminService { - private final ProductRepository productRepository; - - private Product getProductByProductId(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); - } - - public void openProduct(Long productId) { - } -} diff --git a/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java new file mode 100644 index 00000000..98fd6249 --- /dev/null +++ b/src/main/java/poomasi/domain/review/controller/product/ProductReviewController.java @@ -0,0 +1,35 @@ +package poomasi.domain.review.controller.product; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +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.RequestBody; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.service.product.ProductReviewService; + +@Controller +@RequiredArgsConstructor +public class ProductReviewController { + + private final ProductReviewService productReviewService; + + @GetMapping("/api/products/{productId}/reviews") + public ResponseEntity getProductReviews(@PathVariable Long productId) { + List response = productReviewService.getProductReview(productId); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PostMapping("/api/products/{productId}/reviews") + public ResponseEntity registerProductReview(@PathVariable Long productId, + @RequestBody ReviewRequest reviewRequest) { + Long reviewId = productReviewService.registerProductReview(productId, + reviewRequest); + return new ResponseEntity<>(reviewId, HttpStatus.CREATED); + } +} diff --git a/src/main/java/poomasi/domain/review/dto/ReviewRequest.java b/src/main/java/poomasi/domain/review/dto/ReviewRequest.java new file mode 100644 index 00000000..0e61fa12 --- /dev/null +++ b/src/main/java/poomasi/domain/review/dto/ReviewRequest.java @@ -0,0 +1,13 @@ +package poomasi.domain.review.dto; + +import poomasi.domain.review.entity.Review; + +public record ReviewRequest( + Float rating, + String content +) { + + public Review toEntity(Long entityId) { + return new Review(this.rating, this.content, entityId); + } +} diff --git a/src/main/java/poomasi/domain/review/entity/EntityType.java b/src/main/java/poomasi/domain/review/entity/EntityType.java new file mode 100644 index 00000000..d857fabd --- /dev/null +++ b/src/main/java/poomasi/domain/review/entity/EntityType.java @@ -0,0 +1,6 @@ +package poomasi.domain.review.entity; + +public enum EntityType { + PRODUCT, + FARM +} diff --git a/src/main/java/poomasi/domain/review/entity/Review.java b/src/main/java/poomasi/domain/review/entity/Review.java new file mode 100644 index 00000000..b72da487 --- /dev/null +++ b/src/main/java/poomasi/domain/review/entity/Review.java @@ -0,0 +1,63 @@ +package poomasi.domain.review.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.review.dto.ReviewRequest; + +@Entity +@Getter +@NoArgsConstructor +public class Review { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("별점") + private Float rating; + + @Comment("리뷰 내용") + private String content; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + @Comment("엔티티 아이디") + private Long entityId; + + @Comment("엔티티 타입") + @Enumerated(EnumType.STRING) + private EntityType entityType; + +// @Comment("작성자") +// @ManyToOne +// private Member reviewer; + + public Review(Float rating, String content, Long entityId) { + this.rating = rating; + this.content = content; + this.entityId = entityId; + } + + public void modifyReview(ReviewRequest reviewRequest) { + this.rating = reviewRequest.rating(); + this.content = reviewRequest.content(); + } + + public void setReviewType(EntityType entityType) { + this.entityType = entityType; + } +} diff --git a/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java new file mode 100644 index 00000000..cadec05c --- /dev/null +++ b/src/main/java/poomasi/domain/review/service/product/ProductReviewService.java @@ -0,0 +1,47 @@ +package poomasi.domain.review.service.product; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.domain.review.dto.ReviewRequest; +import poomasi.domain.review.dto.ReviewResponse; +import poomasi.domain.review.entity.EntityType; +import poomasi.domain.review.entity.Review; +import poomasi.domain.review.repository.ReviewRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ProductReviewService { + + private final ReviewRepository reviewRepository; + private final ProductRepository productRepository; + + public List getProductReview(Long productId) { + getProductByProductId(productId); //상품이 존재하는지 체크 + + return reviewRepository.findByProductId(productId).stream() + .map(ReviewResponse::fromEntity).toList(); + } + + @Transactional + public Long registerProductReview(Long entityId, ReviewRequest reviewRequest) { + // s3 이미지 저장하고 주소 받아와서 review에 추가해주기 + + Review pReview = reviewRequest.toEntity(entityId); + pReview.setReviewType(EntityType.PRODUCT); + pReview = reviewRepository.save(pReview); + + return pReview.getId(); + } + + private Product getProductByProductId(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); + } + +} From 2e47e3a8cf513b9261b7dce9a82bf7d0bf131247 Mon Sep 17 00:00:00 2001 From: amm0124 <108533909+amm0124@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:24:33 +0900 Subject: [PATCH 14/17] =?UTF-8?q?Feature/issue=2049=20-=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EB=B0=8F=20=EC=9D=B8=EA=B0=80=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: LoginRequest -> SignUpRequest 네임 변경 #39 * refactor: 회원가입 토큰생성 로직 삭제 #39 * refactor: 로그아웃 로직 삭제 #39 * chore: JwtUtil auth 폴더로 이동 #39 * JwtUtil 리팩토링 및 claims에 id, Type 추가 #39 * refactor: JwtUtil 리팩토링 #39 * feat : OAuth2 response interface 구현 * refactoring : member entity 변수 명 변경 및 builder 추가 * refactoring : member profile 기본 생성자 추가 * refactoring : member repository 메서드 * refactoring : jwt 검증 필터 * refactoring : 패키지 구조 변경을 위한 삭제 * refactoring : 로그아웃 필터 주석처리 * refactoring : User Detail 구현체 Oauth2User도 구현 받도록 변경 * feat : 회원가입 패키지 추가 * refactoring : username password 필터 리팩토링 - 메서드 변경 * refactoring : security 설정 변경 * feat : refresh token 관련 (reissue, refresh) 패키지 추가 * refactoring : 안 쓰는 request 삭제 * feat : blacklist 패키지 구조 * feat : 소셜 로그인 user detail service 구현 및 success handler(토큰 발급) * feat : 회원가입 response dto 구현 * refactoring : jwt util 객체 변경 * delete : 안 쓰는 response import 삭제 * delete : 안 쓰는 response import 삭제 * refactoring : test를 위한 경로 open * refactor: Member엔티티 리팩토링 #49 * refactor: MemberRepository 안쓰는 메소드 삭제 #49 * refactor: 토큰 reissue 리팩토링 #49 * refactor: 리프레시 토큰 저장 인터페이스 구현하여 분리 #49 * refactor: JwtUtil 수정 #49 * refactor: 블랙리스트 인터페이스 구현하여 분리 #49 * feat: jpa 토큰 만료 로직 추가 #49 * refactor: 레디스 에러 핸들링 공통화 #49 * feat: jpa 토큰 저장 스케쥴러 구현 #49 * feat: blacklist 및 refreshtoken config 설정 #49 * refactor: JwtUtil 수정 #49 * refactor: 빈 이름 중복 해결 #49 * Delete src/test/java/poomasi/global/config/s3 directory * docs: build.gradle 중복 제거 #49 * refactor: 리프레시 토큰 key 변수 네임 변경 #49 * refactor:레디스, jpa 전환 프로파일 -> application 속성 이용으로 변경 #49 * fix: blacklist key->tokenKey로 변경 #49 * feat : username password authentication filter 완료 * feat : jwt filter 완성 * feat : jwt token을 위한 utility 수정 * feat : oauth2 응답을 위한 response dto 및 인터페이스 생성 * refactoring : member profile 변경 * refactoring : 회원가입 서비스 변경 * feat : 인가 확인 테스트 컨트롤러 작성 * feat : 로그아웃 성공 핸들러 * feat : oauth2 성공 핸들러 * refactoring : bean generator 수정 및 config 수정 * feat : logout filter 작성 * refactoring : username password 필터 수정 - username, password를 받아올 때, object mapper 사용 * refactoring : jwt util 수정 * refactoring : OAuth2 user detail 및 response 구현 * feat : 로그아웃 필터 작성 * refactoring : user detail get Member 추가 * refactoring : 회원가입 코드 변경 * refactoring : security bean, config 수정 - jwt util 의존성 추가 * feat : 로그아웃 handler 구현 * refactoring : jwt 필터 수정 * feat : test controller 구현 * conflict : conflict 해결 및 test controller 구현 * merge : master merge를 위한 commit. conflict 해결 * refactoring : redis global 이동 * refactoring : member profile column 추가 - 상세 주소, x좌표, y좌표 * refactoring : sout -> log 변경 * refactor: JwtUtil 약간 수정#49 * feat : 모든 경로에 대해서 security 적용 해제(임시) --------- Co-authored-by: 정진택 <87135698+jjt4515@users.noreply.github.com> Co-authored-by: amm0124 --- build.gradle | 1 - .../auth/config/SecurityBeanGenerator.java | 25 ++-- .../domain/auth/config/SecurityConfig.java | 111 +++++++++++---- .../auth/controller/AuthController.java | 36 ----- .../domain/auth/dto/request/LoginRequest.java | 4 - .../domain/auth/dto/request/TokenRequest.java | 4 - .../auth/dto/response/TokenResponse.java | 5 - .../domain/auth/entity/RefreshToken.java | 46 ------- .../auth/security/AuthTestController.java | 42 ++++++ .../security/filter/CustomLogoutFilter.java | 63 ++++++++- ...mUsernamePasswordAuthenticationFilter.java | 50 +++++-- .../filter/JwtAuthenticationFilter.java | 60 +++++---- .../handler/ClearAuthenticationHandler.java | 17 +++ .../handler/CookieClearingLogoutHandler.java | 27 ++++ .../handler/CustomLogoutSuccessHandler.java | 29 ++++ .../handler/CustomSuccessHandler.java | 51 +++++++ .../dto/response/OAuth2KakaoResponse.java | 36 +++++ .../oauth2/dto/response/OAuth2Response.java | 10 ++ .../OAuth2UserDetailServiceImpl.java | 79 +++++++++++ .../security/userdetail/UserDetailsImpl.java | 46 ++++--- .../domain/auth/service/AuthService.java | 60 --------- .../auth/service/RefreshTokenService.java | 70 ---------- .../signup/controller/SignupController.java | 25 ++++ .../signup/dto/request/SignupRequest.java | 4 + .../signup/dto/response/SignUpResponse.java | 4 + .../auth/signup/service/SignupService.java | 44 ++++++ .../config/TokenBlacklistServiceConfig.java | 24 ++++ .../token/blacklist/entity/Blacklist.java | 28 ++++ .../repository/BlacklistRepository.java | 17 +++ .../service/BlacklistJpaService.java | 53 ++++++++ .../service/BlacklistRedisService.java | 48 +++++++ .../service/TokenBlacklistService.java | 15 +++ .../domain/auth/token/entity/TokenType.java | 6 + .../config/TokenStorageServiceConfig.java | 24 ++++ .../refreshtoken/entity/RefreshToken.java | 27 ++++ .../repository/TokenRepository.java | 15 +++ .../service/RefreshTokenService.java | 39 ++++++ .../refreshtoken/service/TokenJpaService.java | 46 +++++++ .../service/TokenRedisService.java | 92 +++++++++++++ .../service/TokenStorageService.java | 13 ++ .../controller/ReissueTokenController.java | 23 ++++ .../token/reissue/dto/ReissueRequest.java | 4 + .../token/reissue/dto/ReissueResponse.java | 4 + .../reissue/service/ReissueTokenService.java | 44 ++++++ .../auth/token}/util/JwtUtil.java | 126 +++++++++++++----- .../token/util/TokenCleanupScheduler.java | 29 ++++ .../poomasi/domain/member/entity/Member.java | 27 ++-- .../domain/member/entity/MemberProfile.java | 24 +++- .../member/repository/MemberRepository.java | 4 - .../redis/config/RedisConfig.java | 2 +- .../redis/error/RedisConnectionException.java | 6 +- .../redis/error/RedisExceptionHandler.java | 21 +++ .../redis/error/RedisOperationException.java | 6 +- .../global/redis/service/RedisService.java | 110 --------------- src/main/resources/application.yml | 8 ++ 55 files changed, 1338 insertions(+), 496 deletions(-) delete mode 100644 src/main/java/poomasi/domain/auth/controller/AuthController.java delete mode 100644 src/main/java/poomasi/domain/auth/dto/request/LoginRequest.java delete mode 100644 src/main/java/poomasi/domain/auth/dto/request/TokenRequest.java delete mode 100644 src/main/java/poomasi/domain/auth/dto/response/TokenResponse.java delete mode 100644 src/main/java/poomasi/domain/auth/entity/RefreshToken.java create mode 100644 src/main/java/poomasi/domain/auth/security/AuthTestController.java create mode 100644 src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java create mode 100644 src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java create mode 100644 src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java create mode 100644 src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java create mode 100644 src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java create mode 100644 src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java create mode 100644 src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java delete mode 100644 src/main/java/poomasi/domain/auth/service/AuthService.java delete mode 100644 src/main/java/poomasi/domain/auth/service/RefreshTokenService.java create mode 100644 src/main/java/poomasi/domain/auth/signup/controller/SignupController.java create mode 100644 src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java create mode 100644 src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java create mode 100644 src/main/java/poomasi/domain/auth/signup/service/SignupService.java create mode 100644 src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java create mode 100644 src/main/java/poomasi/domain/auth/token/blacklist/entity/Blacklist.java create mode 100644 src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java create mode 100644 src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java create mode 100644 src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java create mode 100644 src/main/java/poomasi/domain/auth/token/blacklist/service/TokenBlacklistService.java create mode 100644 src/main/java/poomasi/domain/auth/token/entity/TokenType.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java create mode 100644 src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java create mode 100644 src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java create mode 100644 src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java create mode 100644 src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java create mode 100644 src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java rename src/main/java/poomasi/{global => domain/auth/token}/util/JwtUtil.java (54%) create mode 100644 src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java rename src/main/java/poomasi/global/{ => config}/redis/config/RedisConfig.java (96%) rename src/main/java/poomasi/global/{ => config}/redis/error/RedisConnectionException.java (51%) create mode 100644 src/main/java/poomasi/global/config/redis/error/RedisExceptionHandler.java rename src/main/java/poomasi/global/{ => config}/redis/error/RedisOperationException.java (50%) delete mode 100644 src/main/java/poomasi/global/redis/service/RedisService.java diff --git a/build.gradle b/build.gradle index 323e9587..86ba4c57 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // H2 Database diff --git a/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java b/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java index 464b8cd6..2a067e4d 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java @@ -1,21 +1,29 @@ package poomasi.domain.auth.config; import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import poomasi.global.redis.service.RedisService; -import poomasi.global.util.JwtUtil; +import poomasi.domain.auth.token.blacklist.service.TokenBlacklistService; +import poomasi.domain.auth.token.refreshtoken.service.TokenStorageService; +import poomasi.domain.auth.token.util.JwtUtil; +import poomasi.domain.auth.token.refreshtoken.service.TokenRedisService; +import poomasi.domain.member.service.MemberService; +@RequiredArgsConstructor @Configuration public class SecurityBeanGenerator { - @Autowired - private RedisService redisService; + private final TokenStorageService tokenStorageService; + private final MemberService memberService; + private final TokenBlacklistService tokenBlacklistService; @Bean @Description("AuthenticationProvider를 위한 Spring bean") @@ -30,9 +38,10 @@ MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { } @Bean - @Description("jwt 토큰 발급을 위한 spring bean") - JwtUtil jwtProvider() { - return new JwtUtil(redisService); + JwtUtil jwtUtil(){ + return new JwtUtil(tokenBlacklistService, + tokenStorageService, + memberService); } -} +} diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index d857ca2f..7fb751fd 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -1,8 +1,10 @@ package poomasi.domain.auth.config; import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Description; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -13,40 +15,45 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import poomasi.domain.auth.security.filter.CustomLogoutFilter; import poomasi.domain.auth.security.filter.CustomUsernamePasswordAuthenticationFilter; import poomasi.domain.auth.security.filter.JwtAuthenticationFilter; -import poomasi.global.util.JwtUtil; +import poomasi.domain.auth.security.handler.CustomSuccessHandler; +import poomasi.domain.auth.security.userdetail.OAuth2UserDetailServiceImpl; +import poomasi.domain.auth.security.handler.*; +import poomasi.domain.auth.token.util.JwtUtil; @AllArgsConstructor @Configuration @EnableWebSecurity -@EnableMethodSecurity(securedEnabled = false, prePostEnabled = false) // 인가 처리에 대한 annotation +@EnableMethodSecurity(securedEnabled = true , prePostEnabled = false) // 인가 처리에 대한 annotation public class SecurityConfig { private final AuthenticationConfiguration authenticationConfiguration; private final JwtUtil jwtUtil; private final MvcRequestMatcher.Builder mvc; + private final CustomSuccessHandler customSuccessHandler; + + @Autowired + private OAuth2UserDetailServiceImpl oAuth2UserDetailServiceImpl; @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } + @Description("순서 : Oauth2 -> jwt -> login -> logout") @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // TODO : 나중에 허용될 endpoint가 많아지면 whiteList로 관리 예정 - // 임시로 GET : [api/farms, api/products, api/login, api/signup, /]은 열어둠 - http.authorizeHttpRequests((authorize) -> authorize - .requestMatchers(HttpMethod.GET, "/api/farm/**").permitAll() - .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() - .requestMatchers("/api/login", "/", "/api/signup").permitAll() - .anyRequest(). - authenticated() - ); + //form login disable + http.formLogin(AbstractHttpConfigurer::disable); + + //basic login disable + http.httpBasic(AbstractHttpConfigurer::disable); //csrf 해제 http.csrf(AbstractHttpConfigurer::disable); @@ -54,31 +61,77 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti //cors 해제 http.cors(AbstractHttpConfigurer::disable); - //session 해제 -> jwt token 로그인 - http.sessionManagement(session -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - ); - - //Oauth2.0 소셜 로그인 필터 구현 + //세션 해제 + http.sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + //기본 로그아웃 해제 + http.logout(AbstractHttpConfigurer::disable); - //jwt 인증 필터 구현 - http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), CustomUsernamePasswordAuthenticationFilter.class); + /* + // 기본 경로 및 테스트 경로 + http.authorizeHttpRequests((authorize) -> authorize + .requestMatchers(HttpMethod.GET, "/api/farm/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/review/**").permitAll() + .requestMatchers("/api/sign-up", "/api/login", "api/reissue").permitAll() + .requestMatchers("/api/need-auth/**").authenticated() + .anyRequest(). + authenticated() + );*/ - //로그인 filter 구현 - http.addFilterAt(new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class); - //form login disable - http.formLogin(AbstractHttpConfigurer::disable); + http.authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/**").permitAll() + .requestMatchers("/api/need-auth/**").authenticated() + .anyRequest() + .authenticated() + ); - //basic login disable - http.httpBasic(AbstractHttpConfigurer::disable); + /* + 로그아웃 필터 등록하기 + LogoutHandler[] handlers = { + new CookieClearingLogoutHandler(), + new ClearAuthenticationHandler() + }; + CustomLogoutFilter customLogoutFilter = new CustomLogoutFilter(jwtUtil, new CustomLogoutSuccessHandler(), handlers); + customLogoutFilter.setFilterProcessesUrl("/api/logout"); + customLogoutFilter. + http.addFilterAt(customLogoutFilter, LogoutFilter.class); + + http.logout( (logout) -> + logout. + logoutSuccessHandler(new CustomLogoutSuccessHandler()) + .addLogoutHandler(new CookieClearingLogoutHandler()) + .addLogoutHandler(new ClearAuthenticationHandler()) + ); + */ + + /* + oauth2 인증은 현재 해제해놨습니다 -> 차후 code를 front에서 어떤 경로로 받을 것인지 + 아니면 kakao에서 바로 redirect를 백엔드로 할 지 정해지면 + processing url 작성하겠습니다 + + http + .oauth2Login((oauth2) -> oauth2 + .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig + .userService(oAuth2UserDetailServiceImpl)) + .successHandler(customSuccessHandler) + ); + */ + http.oauth2Login(AbstractHttpConfigurer::disable); + + CustomUsernamePasswordAuthenticationFilter customUsernameFilter = + new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil); + customUsernameFilter.setFilterProcessesUrl("/api/login"); + + http.addFilterAt(customUsernameFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); + //http.addFilterAfter(customLogoutFilter, JwtAuthenticationFilter.class); - //log out filter 추가 - //http.addFilterBefore(new CustomLogoutFilter(), CustomLogoutFilter.class); return http.build(); - } + } diff --git a/src/main/java/poomasi/domain/auth/controller/AuthController.java b/src/main/java/poomasi/domain/auth/controller/AuthController.java deleted file mode 100644 index 4f486082..00000000 --- a/src/main/java/poomasi/domain/auth/controller/AuthController.java +++ /dev/null @@ -1,36 +0,0 @@ -package poomasi.domain.auth.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.auth.dto.request.TokenRequest; -import poomasi.domain.auth.dto.response.TokenResponse; -import poomasi.domain.auth.service.AuthService; -import poomasi.domain.auth.dto.request.LoginRequest; - -import static poomasi.domain.member.entity.LoginType.LOCAL; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api") -public class AuthController { - - private final AuthService authService; - - // 일반, 구매자 회원 가입 - @PostMapping("/sign-up") - public ResponseEntity signUp(@RequestBody LoginRequest loginRequest) { - TokenResponse responseBody = authService.signUp(loginRequest, LOCAL); - return ResponseEntity.ok() - .header("Authorization", "Bearer " + responseBody.accessToken()) - .body(responseBody); - } - - @DeleteMapping("/logout/{memberId}") - public ResponseEntity logout(@PathVariable Long memberId, @RequestBody TokenRequest tokenRequest) { - authService.logout(memberId, tokenRequest.accessToken()); - return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); - } - -} diff --git a/src/main/java/poomasi/domain/auth/dto/request/LoginRequest.java b/src/main/java/poomasi/domain/auth/dto/request/LoginRequest.java deleted file mode 100644 index e2311e0d..00000000 --- a/src/main/java/poomasi/domain/auth/dto/request/LoginRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.auth.dto.request; - -public record LoginRequest(String email, String password) { -} diff --git a/src/main/java/poomasi/domain/auth/dto/request/TokenRequest.java b/src/main/java/poomasi/domain/auth/dto/request/TokenRequest.java deleted file mode 100644 index c48fe428..00000000 --- a/src/main/java/poomasi/domain/auth/dto/request/TokenRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package poomasi.domain.auth.dto.request; - -public record TokenRequest(String accessToken) { -} diff --git a/src/main/java/poomasi/domain/auth/dto/response/TokenResponse.java b/src/main/java/poomasi/domain/auth/dto/response/TokenResponse.java deleted file mode 100644 index ac926387..00000000 --- a/src/main/java/poomasi/domain/auth/dto/response/TokenResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package poomasi.domain.auth.dto.response; - -public record TokenResponse(String accessToken, String refreshToken) { - -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/entity/RefreshToken.java b/src/main/java/poomasi/domain/auth/entity/RefreshToken.java deleted file mode 100644 index 61a1dd30..00000000 --- a/src/main/java/poomasi/domain/auth/entity/RefreshToken.java +++ /dev/null @@ -1,46 +0,0 @@ -package poomasi.domain.auth.entity; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import poomasi.global.error.BusinessException; -import poomasi.global.redis.service.RedisService; - -import java.time.Duration; -import java.util.List; - -import static poomasi.global.error.BusinessError.REFRESH_TOKEN_NOT_FOUND; - -@Component -@RequiredArgsConstructor -public class RefreshToken { - - private final RedisService redisService; - - @Value("${jwt.refresh-token-expiration-time}") - private long REFRESH_TOKEN_EXPIRE_TIME; - - public void putRefreshToken(final String refreshToken, Long memberId) { - String redisKey = generateKey(memberId, refreshToken); - redisService.setValues(redisKey, memberId.toString(), Duration.ofSeconds(REFRESH_TOKEN_EXPIRE_TIME)); - } - - public Long getRefreshToken(final String refreshToken, Long memberId) { - String redisKey = generateKey(memberId, refreshToken); - String result = redisService.getValues(redisKey) - .orElseThrow(() -> new BusinessException(REFRESH_TOKEN_NOT_FOUND)); - return Long.parseLong(result); - } - - public void removeMemberRefreshToken(final Long memberId) { - List keys = redisService.scanKeysByPattern(generateKey(memberId, "*")); - for (String key : keys) { - redisService.deleteValues(key); - } - } - - private String generateKey(Long memberId, String token) { - return "refreshToken:" + memberId + ":" + token; - } - -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/AuthTestController.java b/src/main/java/poomasi/domain/auth/security/AuthTestController.java new file mode 100644 index 00000000..64f0d844 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/AuthTestController.java @@ -0,0 +1,42 @@ +package poomasi.domain.auth.security; + + +import jdk.jfr.Description; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; + +@Slf4j +@Description("접근 제어 확인 controller") +@RestController +public class AuthTestController { + + @Secured("ROLE_CUSTOMER") + @GetMapping("/api/auth-test/customer") + public String customer() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + + log.info("email : " + member.getEmail()); + + return "hi. customer"; + } + + @Secured("ROLE_FARMER") + @GetMapping("/api/need-auth/farmer") + public String farmer() { + return "hi. farmer"; + } + + @GetMapping("/api/need-auth") + public String needAuth() { + return "auth"; + } + +} diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java index 96060614..5843d8f6 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java @@ -1,21 +1,70 @@ + + package poomasi.domain.auth.security.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.web.filter.OncePerRequestFilter; +import poomasi.domain.auth.token.util.JwtUtil; import java.io.IOException; +import java.io.PrintWriter; + +@Slf4j +public class CustomLogoutFilter extends LogoutFilter { + + private JwtUtil jwtUtil; + + public CustomLogoutFilter(JwtUtil jwtUtil, LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) { + super(logoutSuccessHandler, handlers); + this.jwtUtil=jwtUtil; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); + } + + public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + + log.info("[logout filter] - 로그아웃 진행합니다."); + + // POST : /api/logout 아니라면 넘기기 + String requestURI = request.getRequestURI(); + String requestMethod = request.getMethod(); + if (!"/api/logout".equals(requestURI) || !requestMethod.equals("POST")) { + log.info("[logout url not matching] "); + filterChain.doFilter(request, response); + return; + } + + + boolean isLogoutSuccess = true; -//extends OncePerRequestFilter -public class CustomLogoutFilter { + if(isLogoutSuccess){ + PrintWriter out = response.getWriter(); + out.println("logout success~. "); + return; + } - /*@Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - // access token 블랙리스트 저장해야 함 - // refresh token 삭제해야 함 + /* + * 로그아웃 로직 + * access token , refresh token 관리하기 + * */ + PrintWriter out = response.getWriter(); + out.println("logout success~. "); + //return; + //filterChain.doFilter(request, response); } -*/ } diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java index cd6c1d02..27b81b61 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java @@ -1,11 +1,14 @@ package poomasi.domain.auth.security.filter; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jdk.jfr.Description; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -13,8 +16,15 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.global.util.JwtUtil; +import poomasi.domain.auth.token.util.JwtUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; + +@Slf4j @RequiredArgsConstructor public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @@ -24,34 +34,56 @@ public class CustomUsernamePasswordAuthenticationFilter extends UsernamePassword @Description("인증 시도 메서드") @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - String username = obtainUsername(request); - String password = obtainPassword(request); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); - return authenticationManager.authenticate(authToken); + + log.info("email - password 기반으로 인증을 시도 합니다 : CustomUsernamePasswordAuthenticationFilter"); + ObjectMapper loginRequestMapper = new ObjectMapper(); + String email = null; + String password = null; + + try { + BufferedReader reader = request.getReader(); + Map credentials = loginRequestMapper.readValue(reader, Map.class); + email = credentials.get("email"); + password = credentials.get("password"); + log.info("유저 정보를 출력합니다. email : "+ email + "password : " + password); + } catch (IOException e) { + throw new RuntimeException(e); + } + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password); + log.info("CustomUsernamePasswordAuthenticationFilter : authentication token 생성 완료"); + return this.authenticationManager.authenticate(authToken); + } @Override @Description("로그인 성공 시, accessToken과 refreshToken 발급") - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { UserDetailsImpl customUserDetails = (UserDetailsImpl) authentication.getPrincipal(); String username = customUserDetails.getUsername(); String role = customUserDetails.getAuthority(); + Long memberId = customUserDetails.getMember().getId(); - String accessToken = "";//jwtUtil.generateAccessToken(username, role); - String refreshToken = "";//jwtUtil.generateRefreshToken(username, role); + String accessToken = jwtUtil.generateTokenInFilter(username, role, "access", memberId); + String refreshToken = jwtUtil.generateTokenInFilter(username, role, "refresh", memberId); + log.info("usename password 기반 로그인 성공 . cookie에 토큰을 넣어 발급합니다."); response.setHeader("access", accessToken); response.addCookie(createCookie("refresh", refreshToken)); response.setStatus(HttpStatus.OK.value()); + + // 나중에 주석 해야 함 + PrintWriter out = response.getWriter(); + out.println("access : " + accessToken + ", refresh : " + refreshToken); + out.close(); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { + log.info("usename password 기반 로그인 실패. "); response.setStatus(401); } private Cookie createCookie(String key, String value) { - Cookie cookie = new Cookie(key, value); cookie.setMaxAge(24*60*60); cookie.setHttpOnly(true); diff --git a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java index 2fd76caf..ec642635 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java @@ -7,13 +7,20 @@ import jdk.jfr.Description; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.util.JwtUtil; import poomasi.domain.member.entity.Member; import poomasi.domain.member.entity.Role; -import poomasi.global.util.JwtUtil; import java.io.IOException; import java.io.PrintWriter; +import java.util.Collection; @Description("access token을 검증하는 필터") @AllArgsConstructor @@ -21,14 +28,25 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; - + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String accessToken = request.getHeader("access"); + log.info("jwt 인증 필터입니다"); + String requestHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + String accessToken = null; + + if (requestHeader == null || !requestHeader.startsWith("Bearer ")) { + log.info("access token을 header로 갖지 않았으므로 다음 usernamepassword 필터로 이동합니다"); + filterChain.doFilter(request, response); + }else{ + //access 추출하기 + log.info("access token 추출하기"); + accessToken = requestHeader.substring(7); + } + + log.info("access token 추출 완료: " + accessToken); - // refresh 재발급이나 다른 요청에 대해서 넘어감 - // access <~token~> if (accessToken == null) { log.info("access token이 존재하지 않아서 다음 filter로 넘어갑니다."); filterChain.doFilter(request, response); @@ -45,37 +63,29 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } // 유효성 검사 - if(jwtUtil.validateToken(accessToken)) { - log.warn("[인증 실패] - 위조된 토큰입니다."); - PrintWriter writer = response.getWriter(); - writer.print("위조된 토큰입니다."); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - - // access token 추출하기 - String tokenType = "";//jwtUtil.getTokenTypeFromToken(accessToken); - - if(!tokenType.equals("access")){ - log.info("[인증 실패] - 위조된 토큰입니다."); + if(!jwtUtil.validateTokenInFilter(accessToken)) { + log.warn("JWT 필터 - [인증 실패] - 위조된 토큰입니다."); PrintWriter writer = response.getWriter(); writer.print("위조된 토큰입니다."); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } - //String username = jwtUtil.getEmailFromToken(accessToken); - //String role = jwtUtil.getRoleFromToken(accessToken); + log.info("토큰 검증 완료"); + String username = jwtUtil.getEmailFromTokenInFilter(accessToken); + String role = jwtUtil.getRoleFromTokenInFilter(accessToken); - //TODO : Object, Object, Collection 형태 ..처리 해야 함 - //TODO : userDetailsImpl(), null(password) - //TODO : security context에 저장해야 함. - //Member member = new Member(username, Role.valueOf(role)); + Member member = new Member(username, Role.valueOf(role)); + UserDetailsImpl userDetailsImpl = new UserDetailsImpl(member); + // (ID, password, auth) + Authentication authToken = new UsernamePasswordAuthenticationToken(userDetailsImpl, null, userDetailsImpl.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + filterChain.doFilter(request, response); + } - } } diff --git a/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java b/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java new file mode 100644 index 00000000..f4ceee19 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java @@ -0,0 +1,17 @@ +package poomasi.domain.auth.security.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.LogoutHandler; + +@Slf4j +public class ClearAuthenticationHandler implements LogoutHandler { + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + log.info("[logout handler] - security context 제거"); + SecurityContextHolder.clearContext(); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java new file mode 100644 index 00000000..4a9dd8a9 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java @@ -0,0 +1,27 @@ +package poomasi.domain.auth.security.handler; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; + +@Slf4j +public class CookieClearingLogoutHandler implements LogoutHandler { + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + cookie.setValue(null); + cookie.setMaxAge(0); // 쿠키 제거 + cookie.setPath("/"); // 적용할 경로 설정 + response.addCookie(cookie); + } + log.info("Cookies cleared"); + } + log.info("[logout handler] - cookie 제거"); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java new file mode 100644 index 00000000..351bd24a --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java @@ -0,0 +1,29 @@ +package poomasi.domain.auth.security.handler; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import java.io.IOException; + +@Slf4j +public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + + log.info("[logout success handler] - cookie 제거"); + expireCookie(response, "access"); + expireCookie(response, "refresh"); + } + + private void expireCookie(HttpServletResponse response, String key) { + Cookie cookie = new Cookie(key, null); // 쿠키를 null로 설정 + cookie.setMaxAge(0); // 쿠키의 최대 생명 주기를 0으로 설정 + cookie.setPath("/"); // 쿠키의 경로를 설정 (원래 설정한 경로와 동일하게) + response.addCookie(cookie); // 응답에 쿠키 추가 + } +} diff --git a/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java new file mode 100644 index 00000000..829e00bd --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java @@ -0,0 +1,51 @@ +package poomasi.domain.auth.security.handler; + +/* + * TODO : Oauth2.0 로그인이 성공하면 access, refresh를 발급해야 함. + * + * */ + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jdk.jfr.Description; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + @Description("TODO : Oauth2.0 로그인이 성공하면 server access, refresh token을 발급하는 메서드") + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + + // 로직은 완성되었습니다 ~ + // Oauth2.0 로그인이 성공하면 server access, refresh token을 발급하는 메서드 + // + log.info("Oauth2 success handler."); + response.setHeader("access", ""); + response.addCookie(createCookie("refresh", "")); + response.setStatus(HttpStatus.OK.value()); + + } + + private Cookie createCookie(String key, String value) { + + Cookie cookie = new Cookie(key, value); + cookie.setMaxAge(60*60*60); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setHttpOnly(true); + + return cookie; + } +} diff --git a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java new file mode 100644 index 00000000..12739679 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java @@ -0,0 +1,36 @@ +package poomasi.domain.auth.security.oauth2.dto.response; + + +import poomasi.domain.member.entity.LoginType; + +import java.util.Map; + +public record OAuth2KakaoResponse(String id, Map attribute) implements OAuth2Response { + + + public OAuth2KakaoResponse(String id, Map attribute) { + this.id = id; + this.attribute = attribute; + } + + @Override + public String getProviderId() { + return id; + } + + @Override + public String getEmail() { + return String.valueOf(attribute.get("email")); + } + + @Override + public String getName() { + return attribute.get("name").toString(); + } + + @Override + public LoginType getLoginType(){ + return LoginType.KAKAO; + } + +} diff --git a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java new file mode 100644 index 00000000..56497ac7 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java @@ -0,0 +1,10 @@ +package poomasi.domain.auth.security.oauth2.dto.response; + +import poomasi.domain.member.entity.LoginType; + +public interface OAuth2Response { + LoginType getLoginType(); + String getProviderId(); + String getEmail(); + String getName(); +} diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java new file mode 100644 index 00000000..78dda49a --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java @@ -0,0 +1,79 @@ +package poomasi.domain.auth.security.userdetail; + +import jdk.jfr.Description; +import lombok.extern.slf4j.Slf4j; +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 poomasi.domain.auth.security.oauth2.dto.response.OAuth2KakaoResponse; +import poomasi.domain.auth.security.oauth2.dto.response.OAuth2Response; +import poomasi.domain.member.entity.LoginType; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.entity.MemberProfile; +import poomasi.domain.member.entity.Role; +import poomasi.domain.member.repository.MemberRepository; + +import java.util.Map; + +@Service +@Description("소셜 서비스와 로컬 계정 연동 할 것이라면 여기서 연동 해야 함") +@Slf4j +public class OAuth2UserDetailServiceImpl extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + public OAuth2UserDetailServiceImpl(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2User oAuth2User = super.loadUser(userRequest); + OAuth2Response oAuth2UserInfo = null; + + if(userRequest.getClientRegistration().getRegistrationId().equals("kakao")) { + + String providerId = String.valueOf(oAuth2User.getAttributes().get("id")); + oAuth2UserInfo = new OAuth2KakaoResponse( + providerId, + (Map)oAuth2User.getAttributes().get("kakao_account") + ); + } else{ + log.warn("지원하지 않은 로그인 서비스 입니다."); + } + + String providerId = oAuth2UserInfo.getProviderId(); + String email = oAuth2UserInfo.getEmail(); + Role role = Role.ROLE_CUSTOMER; + LoginType loginType = oAuth2UserInfo.getLoginType(); + + + //일단 없으면 가입시키는 쪽으로 구현ㄴ + Member member = memberRepository.findByEmail(email).orElse(null); + if(member == null) { + member = Member.builder() + .email(email) + .role(role) + .loginType(loginType) // loginType에 맞게 변경 + .provideId(providerId) + .memberProfile(new MemberProfile()) + .build(); + + memberRepository.save(member); + + } + + //있다면 그냥 member 등록하기 + + if(member.getLoginType()==LoginType.LOCAL){ + //member.setProviderId(providerId); -> 로그인 시 Id 조회함 + } + + // 카카오 회원으로 로그인이 되어 있다면 -> context에 저장 + return new UserDetailsImpl(member, oAuth2User.getAttributes()); + } + +} diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java index 83d71a21..629e425d 100644 --- a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java +++ b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java @@ -1,53 +1,63 @@ package poomasi.domain.auth.security.userdetail; -import lombok.Getter; +import jdk.jfr.Description; +import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; import poomasi.domain.member.entity.Member; +import poomasi.domain.member.entity.Role; import java.util.ArrayList; import java.util.Collection; +import java.util.Map; +@Description("security context에 저장 될 객체") +@Data +public class UserDetailsImpl implements UserDetails, OAuth2User { -@Getter -public class UserDetailsImpl implements UserDetails { - - //private static final long serialVersionUID = 1L; - private final Member member; + private Member member; + private Collection authorities; + private Map attributes; public UserDetailsImpl(Member member) { this.member = member; } + public UserDetailsImpl(Member member, Map attributes ) { + this.member = member; + this.attributes = attributes; + } + @Override public Collection getAuthorities() { - Collection collection = new ArrayList<>(); + Collection collection = new ArrayList(); collection.add(new GrantedAuthority() { @Override public String getAuthority() { - return member.getRole() - .name(); + return String.valueOf(member.getRole()); } }); - return collection; } + public Role getRole(){ + return member.getRole(); + } + public String getAuthority() { return member.getRole().name(); } @Override public String getPassword() { - return this.member - .getPassword(); + return member.getPassword(); } @Override public String getUsername() { - return this.member - .getEmail(); + return member.getEmail(); } @Override @@ -65,9 +75,13 @@ public boolean isCredentialsNonExpired() { return true; } + //Oauth2 member name @Override - public boolean isEnabled() { - return true; + public String getName() { + return null; } + public Member getMember(){ + return member; + } } diff --git a/src/main/java/poomasi/domain/auth/service/AuthService.java b/src/main/java/poomasi/domain/auth/service/AuthService.java deleted file mode 100644 index 2a585416..00000000 --- a/src/main/java/poomasi/domain/auth/service/AuthService.java +++ /dev/null @@ -1,60 +0,0 @@ -package poomasi.domain.auth.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.dto.response.TokenResponse; -import poomasi.domain.auth.dto.request.LoginRequest; -import poomasi.domain.member.entity.LoginType; -import poomasi.domain.member.repository.MemberRepository; -import poomasi.domain.member.entity.Member; -import poomasi.global.error.BusinessException; -import poomasi.global.redis.service.RedisService; -import poomasi.global.util.JwtUtil; - -import java.time.Duration; -import java.util.Map; -import java.util.Optional; - -import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; -import static poomasi.global.error.BusinessError.*; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AuthService { - - private final MemberRepository memberRepository; - private final JwtUtil jwtUtil; - private final RedisService redisService; - private final PasswordEncoder passwordEncoder; - private final RefreshTokenService refreshTokenService; - - // 카카오 로그인 일단 계정 분리 및 계정 연동 보류 - - // 사업자 등록 번호 검사 로직은 추후 논의 필요 - - @Transactional - public TokenResponse signUp(LoginRequest loginRequest, LoginType loginType) { - // 이메일 중복 되어도 로그인 타입 다르면 중복 x - if (memberRepository.findByEmailAndLoginType(loginRequest.email(), loginType).isPresent()) { - throw new BusinessException(DUPLICATE_MEMBER_EMAIL); - } - - Member newMember = new Member(loginRequest.email(), passwordEncoder.encode(loginRequest.password()), loginType, ROLE_CUSTOMER); - memberRepository.save(newMember); - - Map claims = refreshTokenService.createClaims(loginRequest.email(), ROLE_CUSTOMER); - - return refreshTokenService.getTokenResponse(newMember.getId(), claims); - } - - @Transactional - public void logout(Long memberId, String accessToken) { - refreshTokenService.removeRefreshTokenById(memberId); - - redisService.setBlackList(accessToken, "accessToken", Duration.ofMillis(jwtUtil.getAccessTokenExpiration())); - } - -} diff --git a/src/main/java/poomasi/domain/auth/service/RefreshTokenService.java b/src/main/java/poomasi/domain/auth/service/RefreshTokenService.java deleted file mode 100644 index a5fee8a8..00000000 --- a/src/main/java/poomasi/domain/auth/service/RefreshTokenService.java +++ /dev/null @@ -1,70 +0,0 @@ -package poomasi.domain.auth.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import poomasi.domain.auth.dto.response.TokenResponse; -import poomasi.domain.auth.entity.RefreshToken; -import poomasi.domain.member.entity.Member; -import poomasi.domain.member.entity.Role; -import poomasi.domain.member.repository.MemberRepository; -import poomasi.global.error.BusinessException; -import poomasi.global.util.JwtUtil; - -import java.util.HashMap; -import java.util.Map; - -import static poomasi.global.error.BusinessError.*; - -@Service -@RequiredArgsConstructor -public class RefreshTokenService { - - private final JwtUtil jwtUtil; - private final RefreshToken refreshTokenManager; - private final MemberRepository memberRepository; - - // 토큰 리프레시 - public TokenResponse refreshToken(final String refreshToken) { - String email = jwtUtil.getEmailFromToken(refreshToken); - - Member member = getMemberByEmail(email); - Long memberId = member.getId(); - - checkRefreshToken(refreshToken, memberId); - - Map claims = createClaims(email, member.getRole()); - - return getTokenResponse(memberId, claims); - } - - public TokenResponse getTokenResponse(Long memberId, Map claims) { - String newAccessToken = jwtUtil.generateAccessToken(String.valueOf(memberId), claims); - refreshTokenManager.removeMemberRefreshToken(memberId); - - String newRefreshToken = jwtUtil.generateRefreshToken(String.valueOf(memberId), claims); - refreshTokenManager.putRefreshToken(newRefreshToken, memberId); - - return new TokenResponse(newAccessToken, newRefreshToken); - } - - public void removeRefreshTokenById(Long memberId) { - refreshTokenManager.removeMemberRefreshToken(memberId); - } - - private void checkRefreshToken(final String refreshToken, Long memberId) { - if(!jwtUtil.validateRefreshToken(refreshToken, memberId)) - throw new BusinessException(REFRESH_TOKEN_NOT_VALID); - } - - public Member getMemberByEmail(String email) { - return memberRepository.findByEmail(email) - .orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND)); - } - - public Map createClaims(String email, Role role) { - Map claims = new HashMap<>(); - claims.put("email", email); - claims.put("role", role); - return claims; - } -} diff --git a/src/main/java/poomasi/domain/auth/signup/controller/SignupController.java b/src/main/java/poomasi/domain/auth/signup/controller/SignupController.java new file mode 100644 index 00000000..3be51a8c --- /dev/null +++ b/src/main/java/poomasi/domain/auth/signup/controller/SignupController.java @@ -0,0 +1,25 @@ +package poomasi.domain.auth.signup.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.auth.signup.dto.response.SignUpResponse; +import poomasi.domain.auth.signup.service.SignupService; +import poomasi.domain.auth.signup.dto.request.SignupRequest; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class SignupController { + + private final SignupService signupService; + + @PostMapping("/sign-up") + public ResponseEntity signUp(@RequestBody SignupRequest signupRequest) { + return ResponseEntity.ok(signupService + .signUp(signupRequest)); + } + +} + + diff --git a/src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java b/src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java new file mode 100644 index 00000000..7799d74a --- /dev/null +++ b/src/main/java/poomasi/domain/auth/signup/dto/request/SignupRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.auth.signup.dto.request; + +public record SignupRequest(String email, String password) { +} diff --git a/src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java b/src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java new file mode 100644 index 00000000..70da0d1c --- /dev/null +++ b/src/main/java/poomasi/domain/auth/signup/dto/response/SignUpResponse.java @@ -0,0 +1,4 @@ +package poomasi.domain.auth.signup.dto.response; + +public record SignUpResponse(String email, String message) { +} diff --git a/src/main/java/poomasi/domain/auth/signup/service/SignupService.java b/src/main/java/poomasi/domain/auth/signup/service/SignupService.java new file mode 100644 index 00000000..fe8c0859 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/signup/service/SignupService.java @@ -0,0 +1,44 @@ +package poomasi.domain.auth.signup.service; + +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.signup.dto.request.SignupRequest; +import poomasi.domain.auth.signup.dto.response.SignUpResponse; +import poomasi.domain.member.entity.LoginType; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.domain.member.entity.Member; +import poomasi.global.error.BusinessException; + +import static poomasi.domain.member.entity.Role.ROLE_CUSTOMER; +import static poomasi.global.error.BusinessError.*; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class SignupService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + @Description("카카오톡으로 먼저 회원가입이 되어 있는 경우, 계정 연동을 진행합니다. ") + @Transactional + public SignUpResponse signUp(SignupRequest signupRequest) { + String email = signupRequest.email(); + String password = signupRequest.password(); + + memberRepository.findByEmail(email) + .ifPresent(member -> { throw new BusinessException(DUPLICATE_MEMBER_EMAIL); }); + + Member newMember = new Member(email, + passwordEncoder.encode(password), + LoginType.LOCAL, + ROLE_CUSTOMER); + + memberRepository.save(newMember); + return new SignUpResponse(email, "회원 가입 성공"); + } +} + diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java b/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java new file mode 100644 index 00000000..458c3702 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/config/TokenBlacklistServiceConfig.java @@ -0,0 +1,24 @@ +package poomasi.domain.auth.token.blacklist.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; +import poomasi.domain.auth.token.blacklist.service.TokenBlacklistService; +import poomasi.domain.auth.token.blacklist.service.BlacklistRedisService; + +@Configuration +public class TokenBlacklistServiceConfig { + + @Value("${spring.token.blacklist.type}") + private String tokenBlacklistType; + + @Bean + public TokenBlacklistService tokenBlacklistService(BlacklistRedisService blacklistRedisService, BlacklistJpaService blacklistJpaService) { + if ("redis".equals(tokenBlacklistType)) { + return blacklistRedisService; + } else { + return blacklistJpaService; + } + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/entity/Blacklist.java b/src/main/java/poomasi/domain/auth/token/blacklist/entity/Blacklist.java new file mode 100644 index 00000000..0e66a2fd --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/entity/Blacklist.java @@ -0,0 +1,28 @@ +package poomasi.domain.auth.token.blacklist.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Blacklist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String tokenKey; + + @Column(nullable = false) + private String data; + + @Column(nullable = false) + private LocalDateTime expireAt; +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java b/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java new file mode 100644 index 00000000..2d3684f8 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java @@ -0,0 +1,17 @@ +package poomasi.domain.auth.token.blacklist.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.auth.token.blacklist.entity.Blacklist; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Repository +public interface BlacklistRepository extends JpaRepository { + void deleteByTokenKey(String key); + Optional findByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); + boolean existsByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); + void deleteAllByExpireAtBefore(LocalDateTime now); + +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java new file mode 100644 index 00000000..6b7bd092 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java @@ -0,0 +1,53 @@ +package poomasi.domain.auth.token.blacklist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.token.blacklist.entity.Blacklist; +import poomasi.domain.auth.token.blacklist.repository.BlacklistRepository; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BlacklistJpaService implements TokenBlacklistService{ + private final BlacklistRepository blacklistRepository; + + @Override + @Transactional + public void setBlackList(String key, String data, Duration duration) { + LocalDateTime expireAt = LocalDateTime.now().plusSeconds(duration.getSeconds()); + + Blacklist blacklist = new Blacklist(); + blacklist.setTokenKey(key); + blacklist.setData(data); + blacklist.setExpireAt(expireAt); + + blacklistRepository.save(blacklist); + } + + @Override + public Optional getBlackList(String key) { + return blacklistRepository.findByTokenKeyAndExpireAtAfter(key, LocalDateTime.now()) + .map(Blacklist::getData); + } + + @Override + @Transactional + public void deleteBlackList(String key) { + blacklistRepository.deleteByTokenKey(key); + } + + @Override + public boolean hasKeyBlackList(String key) { + return blacklistRepository.existsByTokenKeyAndExpireAtAfter(key, LocalDateTime.now()); + } + + @Transactional + public void removeExpiredTokens() { + blacklistRepository.deleteAllByExpireAtBefore(LocalDateTime.now()); + } +} diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java new file mode 100644 index 00000000..369bcbf5 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistRedisService.java @@ -0,0 +1,48 @@ +package poomasi.domain.auth.token.blacklist.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.*; + +import static poomasi.global.config.redis.error.RedisExceptionHandler.handleRedisException; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BlacklistRedisService implements TokenBlacklistService { + private final RedisTemplate redisBlackListTemplate; + + @Transactional + public void setBlackList(String key, String data, Duration duration) { + handleRedisException(() -> { + ValueOperations values = redisBlackListTemplate.opsForValue(); + values.set(key, data, duration); + return null; + }, "블랙리스트에 값을 설정하는 중 오류 발생: " + key); + } + + public Optional getBlackList(String key) { + return handleRedisException(() -> { + ValueOperations values = redisBlackListTemplate.opsForValue(); + Object result = values.get(key); + return Optional.ofNullable(result).map(Object::toString); + }, "블랙리스트에서 값을 가져오는 중 오류 발생: " + key); + } + + @Transactional + public void deleteBlackList(String key) { + handleRedisException(() -> redisBlackListTemplate.delete(key), "블랙리스트에서 값을 삭제하는 중 오류 발생: " + key); + } + + public boolean hasKeyBlackList(String key) { + return handleRedisException(() -> Boolean.TRUE.equals(redisBlackListTemplate.hasKey(key)), "블랙리스트에서 키 존재 여부 확인 중 오류 발생: " + key); + } + +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/service/TokenBlacklistService.java b/src/main/java/poomasi/domain/auth/token/blacklist/service/TokenBlacklistService.java new file mode 100644 index 00000000..519231d7 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/blacklist/service/TokenBlacklistService.java @@ -0,0 +1,15 @@ +package poomasi.domain.auth.token.blacklist.service; + +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Optional; + +@Service +public interface TokenBlacklistService { + void setBlackList(String key, String data, Duration duration); + Optional getBlackList(String key); + void deleteBlackList(String key); + boolean hasKeyBlackList(String key); + +} diff --git a/src/main/java/poomasi/domain/auth/token/entity/TokenType.java b/src/main/java/poomasi/domain/auth/token/entity/TokenType.java new file mode 100644 index 00000000..3b21f148 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/entity/TokenType.java @@ -0,0 +1,6 @@ +package poomasi.domain.auth.token.entity; + +public enum TokenType { + ACCESS, + REFRESH +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java new file mode 100644 index 00000000..75458385 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/config/TokenStorageServiceConfig.java @@ -0,0 +1,24 @@ +package poomasi.domain.auth.token.refreshtoken.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import poomasi.domain.auth.token.refreshtoken.service.TokenJpaService; +import poomasi.domain.auth.token.refreshtoken.service.TokenRedisService; +import poomasi.domain.auth.token.refreshtoken.service.TokenStorageService; + +@Configuration +public class TokenStorageServiceConfig { + + @Value("${spring.token.storage.type}") + private String tokenStorageType; + + @Bean + public TokenStorageService tokenStorageService(TokenRedisService tokenRedisService, TokenJpaService tokenJpaService) { + if ("redis".equals(tokenStorageType)) { + return tokenRedisService; + } else { + return tokenJpaService; + } + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java new file mode 100644 index 00000000..211f178e --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/entity/RefreshToken.java @@ -0,0 +1,27 @@ +package poomasi.domain.auth.token.refreshtoken.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "refresh_tokens") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String tokenKey; + + @Column(nullable = false) + private String data; + + @Column(nullable = false) + private LocalDateTime expireAt; +} diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java new file mode 100644 index 00000000..86062443 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/repository/TokenRepository.java @@ -0,0 +1,15 @@ +package poomasi.domain.auth.token.refreshtoken.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.auth.token.refreshtoken.entity.RefreshToken; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Repository +public interface TokenRepository extends JpaRepository { + void deleteAllByData(String Data); + void deleteAllByExpireAtBefore(LocalDateTime now); + Optional findByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java new file mode 100644 index 00000000..c7111a58 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/RefreshTokenService.java @@ -0,0 +1,39 @@ +package poomasi.domain.auth.token.refreshtoken.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.global.error.BusinessException; + +import java.time.Duration; + +import static poomasi.global.error.BusinessError.REFRESH_TOKEN_NOT_FOUND; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class RefreshTokenService { + + private final TokenStorageService tokenStorageService; + + @Value("${jwt.refresh-token-expiration-time}") + private long REFRESH_TOKEN_EXPIRE_TIME; + + @Transactional + public void putRefreshToken(final String refreshToken, Long memberId) { + tokenStorageService.setValues(refreshToken, memberId.toString(), Duration.ofSeconds(REFRESH_TOKEN_EXPIRE_TIME)); + } + + public Long getRefreshToken(final String refreshToken, Long memberId) { + String result = tokenStorageService.getValues(refreshToken, memberId.toString()) + .orElseThrow(() -> new BusinessException(REFRESH_TOKEN_NOT_FOUND)); + return Long.parseLong(result); + } + + @Transactional + public void removeMemberRefreshToken(final Long memberId) { + tokenStorageService.removeRefreshTokenById(memberId); + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java new file mode 100644 index 00000000..5e5e628f --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenJpaService.java @@ -0,0 +1,46 @@ +package poomasi.domain.auth.token.refreshtoken.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.token.refreshtoken.entity.RefreshToken; +import poomasi.domain.auth.token.refreshtoken.repository.TokenRepository; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TokenJpaService implements TokenStorageService { + + private final TokenRepository tokenRepository; + + @Override + @Transactional + public void setValues(String key, String data, Duration duration) { + RefreshToken tokenEntity = new RefreshToken(); + tokenEntity.setTokenKey(key); + tokenEntity.setData(data); + tokenEntity.setExpireAt(LocalDateTime.now().plusSeconds(duration.getSeconds())); + tokenRepository.save(tokenEntity); + } + + @Override + public Optional getValues(String key, String data) { + return tokenRepository.findByTokenKeyAndExpireAtAfter(key, LocalDateTime.now()) + .map(RefreshToken::getData); + } + + @Override + @Transactional + public void removeRefreshTokenById(final Long memberId) { + tokenRepository.deleteAllByData(String.valueOf(memberId)); + } + + @Transactional + public void removeExpiredTokens() { + tokenRepository.deleteAllByExpireAtBefore(LocalDateTime.now()); + } +} diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java new file mode 100644 index 00000000..96916a4d --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenRedisService.java @@ -0,0 +1,92 @@ +package poomasi.domain.auth.token.refreshtoken.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.global.config.redis.error.RedisOperationException; + +import java.time.Duration; +import java.util.*; + +import static poomasi.global.config.redis.error.RedisExceptionHandler.handleRedisException; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TokenRedisService implements TokenStorageService { + private final RedisTemplate redisTemplate; + private final RedisConnectionFactory redisConnectionFactory; + + @Transactional + public void setValues(String key, String data, Duration duration) { + String redisKey = generateKey(data, key); + handleRedisException(() -> { + ValueOperations values = redisTemplate.opsForValue(); + values.set(redisKey, data, duration); + return null; + }, "Redis에 값을 설정하는 중 오류 발생: " + redisKey); + } + + public Optional getValues(String key, String data) { + String redisKey = generateKey(data, key); + return handleRedisException(() -> { + ValueOperations values = redisTemplate.opsForValue(); + Object result = values.get(redisKey); + return Optional.ofNullable(result).map(Object::toString); + }, "Redis에서 값을 가져오는 중 오류 발생: " + redisKey); + } + + @Transactional + public void removeRefreshTokenById(Long memberId) { + List keys = scanKeysByPattern(generateKey(String.valueOf(memberId), "*")); + for (String key : keys) { + deleteValues(key, memberId.toString()); + } + } + + @Transactional + public void deleteValues(String key, String data) { + String redisKey = generateKey(data, key); + handleRedisException(() -> redisTemplate.delete(redisKey), "Redis에서 값을 삭제하는 중 오류 발생: " + redisKey); + } + + public List scanKeysByPattern(String pattern) { + return handleRedisException(() -> { + List keys = new ArrayList<>(); + ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build(); + + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + Cursor cursor = connection.scan(options); + while (cursor.hasNext()) { + keys.add(new String(cursor.next())); + } + } catch (Exception e) { + throw new RedisOperationException("Redis SCAN 중 오류 발생"); + } + return keys; + }, "SCAN 중 오류 발생: " + pattern); + } + + public boolean hasKey(String key, String data) { + String redisKey = generateKey(data, key); + return handleRedisException(() -> Boolean.TRUE.equals(redisTemplate.hasKey(redisKey)), "Redis에서 키 존재 여부 확인 중 오류 발생: " + redisKey); + } + + public List getKeysByPattern(String pattern) { + Set keys = redisTemplate.keys(pattern); + return keys != null ? new ArrayList<>(keys) : Collections.emptyList(); + } + + private String generateKey(String memberId, String token) { + return "refreshToken:" + memberId + ":" + token; + } + +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java new file mode 100644 index 00000000..1b170b72 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/refreshtoken/service/TokenStorageService.java @@ -0,0 +1,13 @@ +package poomasi.domain.auth.token.refreshtoken.service; + +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Optional; + +@Service +public interface TokenStorageService { + void setValues(String key, String data, Duration duration); + Optional getValues(String key, String data); + void removeRefreshTokenById(final Long memberId); +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java new file mode 100644 index 00000000..fc2dc627 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java @@ -0,0 +1,23 @@ +package poomasi.domain.auth.token.reissue.controller; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.auth.token.reissue.dto.ReissueRequest; +import poomasi.domain.auth.token.reissue.dto.ReissueResponse; +import poomasi.domain.auth.token.reissue.service.ReissueTokenService; + +@RestController +public class ReissueTokenController { + + @Autowired + private ReissueTokenService reissueTokenService; + + @GetMapping("/api/reissue") + public ResponseEntity reissue(@RequestBody ReissueRequest reissueRequest){ + return ResponseEntity.ok(reissueTokenService.reissueToken(reissueRequest)); + } +} diff --git a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java new file mode 100644 index 00000000..c18fb929 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.auth.token.reissue.dto; + +public record ReissueRequest(String refreshToken) { +} diff --git a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java new file mode 100644 index 00000000..258ce50d --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java @@ -0,0 +1,4 @@ +package poomasi.domain.auth.token.reissue.dto; + +public record ReissueResponse(String accessToken, String refreshToken) { +} diff --git a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java new file mode 100644 index 00000000..bc9884fb --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java @@ -0,0 +1,44 @@ +package poomasi.domain.auth.token.reissue.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.auth.token.reissue.dto.ReissueRequest; +import poomasi.domain.auth.token.refreshtoken.service.RefreshTokenService; +import poomasi.domain.auth.token.reissue.dto.ReissueResponse; +import poomasi.global.error.BusinessException; +import poomasi.domain.auth.token.util.JwtUtil; + +import static poomasi.global.error.BusinessError.*; + +@Service +@RequiredArgsConstructor +public class ReissueTokenService { + + private final JwtUtil jwtUtil; + private final RefreshTokenService refreshTokenService; + + // 토큰 재발급 + public ReissueResponse reissueToken(ReissueRequest reissueRequest) { + String refreshToken = reissueRequest.refreshToken(); + Long memberId = jwtUtil.getIdFromToken(refreshToken); + + checkRefreshToken(refreshToken, memberId); + + return getTokenResponse(memberId); + } + + public ReissueResponse getTokenResponse(Long memberId) { + String newAccessToken = jwtUtil.generateAccessTokenById(memberId); + refreshTokenService.removeMemberRefreshToken(memberId); + + String newRefreshToken = jwtUtil.generateRefreshTokenById(memberId); + refreshTokenService.putRefreshToken(newRefreshToken, memberId); + + return new ReissueResponse(newAccessToken, newRefreshToken); + } + + private void checkRefreshToken(final String refreshToken, Long memberId) { + if(!jwtUtil.validateRefreshToken(refreshToken, memberId)) + throw new BusinessException(REFRESH_TOKEN_NOT_VALID); + } +} diff --git a/src/main/java/poomasi/global/util/JwtUtil.java b/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java similarity index 54% rename from src/main/java/poomasi/global/util/JwtUtil.java rename to src/main/java/poomasi/domain/auth/token/util/JwtUtil.java index efe8ab43..46501256 100644 --- a/src/main/java/poomasi/global/util/JwtUtil.java +++ b/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java @@ -1,4 +1,4 @@ -package poomasi.global.util; +package poomasi.domain.auth.token.util; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; @@ -7,14 +7,20 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import poomasi.domain.member.entity.Role; -import poomasi.global.redis.service.RedisService; +import poomasi.domain.auth.token.blacklist.service.TokenBlacklistService; +import poomasi.domain.auth.token.refreshtoken.service.TokenStorageService; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; +import java.util.HashMap; import java.util.Map; +import static poomasi.domain.auth.token.entity.TokenType.ACCESS; +import static poomasi.domain.auth.token.entity.TokenType.REFRESH; + @Component @RequiredArgsConstructor @Slf4j @@ -31,56 +37,112 @@ public class JwtUtil { @Value("${jwt.refresh-token-expiration-time}") private long REFRESH_TOKEN_EXPIRATION_TIME; - private final RedisService redisService; + private final TokenBlacklistService tokenBlacklistService; + private final TokenStorageService tokenStorageService; + private final MemberService memberService; @PostConstruct public void init() { secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } + + public String generateTokenInFilter(String email, String role , String tokenType, Long memberId){ + Map claims = this.createClaimsInFilter(email, role, tokenType); + String memberIdString = memberId.toString(); + + return Jwts.builder() + .setClaims(claims) + .setSubject(memberIdString) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + private Map createClaimsInFilter(String email, String role, String tokenType) { + Map claims = new HashMap<>(); + claims.put("email", email); + claims.put("role", role); + claims.put("tokenType" , tokenType); + return claims; + } + + public Boolean validateTokenInFilter(String token){ + + log.info("jwt util에서 토큰 검증을 진행합니다 . ."); + + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (Exception e) { + log.info("jwt util에서 토큰 검증 하다가 exception 터졌습니다."); + log.info(e.getMessage()); + return false; + } + + } + + public String getRoleFromTokenInFilter(final String token) { + return getClaimFromToken(token, "role", String.class); + } + + public String getEmailFromTokenInFilter(final String token) { + return getClaimFromToken(token, "email", String.class); + } + + // <--------------------------------------------> // 토큰 생성 - public String generateAccessToken(final String memberId, final Map claims) { + + public String generateAccessTokenById(final Long memberId) { + Map claims = createClaims(memberId); + claims.put("type", ACCESS); return Jwts.builder() .setClaims(claims) - .setSubject(memberId) + .setSubject(String.valueOf(memberId)) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } - public String generateRefreshToken(final String memberId, final Map claims) { + public String generateRefreshTokenById(final Long memberId) { + Map claims = createClaims(memberId); + claims.put("type", REFRESH); return Jwts.builder() .setClaims(claims) - .setSubject(memberId) + .setSubject(String.valueOf(memberId)) .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION_TIME)) .setIssuedAt(new Date(System.currentTimeMillis())) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } - // 토큰에서 정보 추출 - public String getSubjectFromToken(final String token) { - return getAllClaimsFromToken(token).getSubject(); - } + public Map createClaims(Long memberId) { + Map claims = new HashMap<>(); + Member member = memberService.findMemberById(memberId); - public String getEmailFromToken(final String token) { - return getClaimFromToken(token, "email", String.class); - } + claims.put("id", memberId); + claims.put("email", member.getEmail()); + claims.put("role", member.getRole()); - public Role getRoleFromToken(final String token) { - return getClaimFromToken(token, "role", Role.class); + return claims; } - public T getClaimFromToken(final String token, String claimKey, Class claimType) { - Claims claims = getAllClaimsFromToken(token); - return claims.get(claimKey, claimType); + // 토큰 이용해서 추출 + public Long getIdFromToken(final String token) { + return getClaimFromToken(token, "id", Long.class); } public Date getExpirationDateFromToken(final String token) { return getAllClaimsFromToken(token).getExpiration(); } + private T getClaimFromToken(final String token, String claimKey, Class claimType) { + Claims claims = getAllClaimsFromToken(token); + return claims.get(claimKey, claimType); + } + private Claims getAllClaimsFromToken(final String token) { return Jwts.parserBuilder() .setSigningKey(secretKey) @@ -90,22 +152,11 @@ private Claims getAllClaimsFromToken(final String token) { } // 토큰 유효성 검사 - public Boolean validateAccessToken(final String accessToken){ - if (!validateToken(accessToken)) { - return false; - } - if (redisService.hasKeyBlackList(accessToken)){ - log.warn("로그아웃한 JWT token입니다."); - return false; - } - return true; - } - public Boolean validateRefreshToken(final String refreshToken, final Long memberId) { if (!validateToken(refreshToken)) { return false; } - String storedMemberId = redisService.getValues(refreshToken) + String storedMemberId = tokenStorageService.getValues(refreshToken, memberId.toString()) .orElse(null); if (storedMemberId == null || !storedMemberId.equals(memberId.toString())) { @@ -116,6 +167,17 @@ public Boolean validateRefreshToken(final String refreshToken, final Long member return true; } + public Boolean validateAccessToken(final String accessToken){ + if (!validateToken(accessToken)) { + return false; + } + if ( tokenBlacklistService.hasKeyBlackList(accessToken)){ + log.warn("로그아웃한 JWT token입니다."); + return false; + } + return true; + } + public Boolean validateToken(final String token) { try { Jwts.parserBuilder() diff --git a/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java b/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java new file mode 100644 index 00000000..fb918879 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/token/util/TokenCleanupScheduler.java @@ -0,0 +1,29 @@ +package poomasi.domain.auth.token.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; +import poomasi.domain.auth.token.refreshtoken.service.TokenJpaService; + +@Component +@RequiredArgsConstructor +public class TokenCleanupScheduler { + private final BlacklistJpaService blacklistJpaService; + private final TokenJpaService tokenJpaService; + + // spring.token.blacklist.type이 "jpa"일 때만 실행 + @Scheduled(fixedRate = 3600000) // 한 시간마다 실행 (1시간 = 3600000 밀리초) + @ConditionalOnProperty(name = "spring.token.blacklist.type", havingValue = "jpa") + public void cleanUpBlacklistExpiredTokens() { + blacklistJpaService.removeExpiredTokens(); + } + + // spring.token.storage.type이 "jpa"일 때만 실행 + @Scheduled(fixedRate = 86400000) // 하루마다 실행 (24시간 = 86400000 밀리초) + @ConditionalOnProperty(name = "spring.token.storage.type", havingValue = "jpa") + public void cleanUpTokenExpiredTokens() { + tokenJpaService.removeExpiredTokens(); + } +} diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index 2f08b496..6db41d0c 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -1,15 +1,13 @@ package poomasi.domain.member.entity; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; - import java.time.LocalDateTime; -import static poomasi.domain.member.entity.LoginType.LOCAL; - @Getter @Entity @Table(name = "member") @@ -38,10 +36,10 @@ public class Member { private Role role; @Column(nullable = true) - private String kakaoAuthId; + private String provideId; @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private MemberProfile profile; + private MemberProfile memberProfile; private LocalDateTime deletedAt; @@ -57,17 +55,20 @@ public Member(String email, Role role){ this.role = role; } - - public void setProfile(MemberProfile profile) { - this.profile = profile; - if (profile != null) { - profile.setMember(this); + public void setMemberProfile(MemberProfile memberProfile) { + this.memberProfile = memberProfile; + if (memberProfile != null) { + memberProfile.setMember(this); } } - public void kakaoToLocal(String password) { - this.password = password; - this.loginType = LOCAL; + @Builder + public Member(String email, Role role, LoginType loginType, String provideId, MemberProfile memberProfile) { + this.email = email; + this.role = role; + this.loginType = loginType; + this.provideId = provideId; + this.memberProfile = memberProfile; } public boolean isFarmer() { diff --git a/src/main/java/poomasi/domain/member/entity/MemberProfile.java b/src/main/java/poomasi/domain/member/entity/MemberProfile.java index 50905cc9..6a599a71 100644 --- a/src/main/java/poomasi/domain/member/entity/MemberProfile.java +++ b/src/main/java/poomasi/domain/member/entity/MemberProfile.java @@ -2,7 +2,6 @@ import jakarta.persistence.*; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; @@ -10,23 +9,31 @@ @Getter @Entity @Table(name = "member_profile") -@NoArgsConstructor public class MemberProfile { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 50) + @Column(nullable = true, length = 50) private String name; - @Column(length = 20) + @Column(nullable = true, length = 20) private String phoneNumber; - @Column(length = 255) + @Column(nullable = true, length = 255) private String address; - @Column(nullable = false) + @Column(nullable = true, length = 255) + private String addressDetail; + + @Column(nullable=true, length=255) + private Long coordinateX; + + @Column(nullable=true, length=255) + private Long coordinateY; + + @Column(nullable = true, length = 50) private boolean isBanned; @Column(nullable = false) @@ -46,4 +53,9 @@ public MemberProfile(String name, String phoneNumber, String address, Member mem this.member = member; } + public MemberProfile() { + this.name = "UNKNOWN"; // name not null 조건 때문에 임시로 넣었습니다. nullable도 true로 넣었는데 안 되네요 + this.createdAt = LocalDateTime.now(); + } + } diff --git a/src/main/java/poomasi/domain/member/repository/MemberRepository.java b/src/main/java/poomasi/domain/member/repository/MemberRepository.java index c1d00baf..f9bcb1c3 100644 --- a/src/main/java/poomasi/domain/member/repository/MemberRepository.java +++ b/src/main/java/poomasi/domain/member/repository/MemberRepository.java @@ -2,15 +2,11 @@ import org.springframework.data.jpa.repository.JpaRepository; -import poomasi.domain.member.entity.LoginType; import poomasi.domain.member.entity.Member; import java.util.Optional; public interface MemberRepository extends JpaRepository { - Optional findByEmailAndLoginType(String email, LoginType loginType); - Optional findByEmail(String email); - Optional findByIdAndDeletedAtIsNull(Long id); } diff --git a/src/main/java/poomasi/global/redis/config/RedisConfig.java b/src/main/java/poomasi/global/config/redis/config/RedisConfig.java similarity index 96% rename from src/main/java/poomasi/global/redis/config/RedisConfig.java rename to src/main/java/poomasi/global/config/redis/config/RedisConfig.java index 3eb833ed..a728995f 100644 --- a/src/main/java/poomasi/global/redis/config/RedisConfig.java +++ b/src/main/java/poomasi/global/config/redis/config/RedisConfig.java @@ -1,4 +1,4 @@ -package poomasi.global.redis.config; +package poomasi.global.config.redis.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/poomasi/global/redis/error/RedisConnectionException.java b/src/main/java/poomasi/global/config/redis/error/RedisConnectionException.java similarity index 51% rename from src/main/java/poomasi/global/redis/error/RedisConnectionException.java rename to src/main/java/poomasi/global/config/redis/error/RedisConnectionException.java index e5a8a823..5161ba30 100644 --- a/src/main/java/poomasi/global/redis/error/RedisConnectionException.java +++ b/src/main/java/poomasi/global/config/redis/error/RedisConnectionException.java @@ -1,11 +1,7 @@ -package poomasi.global.redis.error; +package poomasi.global.config.redis.error; public class RedisConnectionException extends RuntimeException { public RedisConnectionException(String message) { super(message); } - - public RedisConnectionException(String message, Throwable cause) { - super(message, cause); - } } \ No newline at end of file diff --git a/src/main/java/poomasi/global/config/redis/error/RedisExceptionHandler.java b/src/main/java/poomasi/global/config/redis/error/RedisExceptionHandler.java new file mode 100644 index 00000000..2943f270 --- /dev/null +++ b/src/main/java/poomasi/global/config/redis/error/RedisExceptionHandler.java @@ -0,0 +1,21 @@ +package poomasi.global.config.redis.error; + +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Supplier; + +@Slf4j +public class RedisExceptionHandler { + + public static T handleRedisException(Supplier action, String errorMessage) { + try { + return action.get(); + } catch (RedisConnectionException e) { + log.error(errorMessage, e); + throw new RedisConnectionException(errorMessage); + } catch (Exception e) { + log.error(errorMessage, e); + throw new RedisOperationException(errorMessage); + } + } +} \ No newline at end of file diff --git a/src/main/java/poomasi/global/redis/error/RedisOperationException.java b/src/main/java/poomasi/global/config/redis/error/RedisOperationException.java similarity index 50% rename from src/main/java/poomasi/global/redis/error/RedisOperationException.java rename to src/main/java/poomasi/global/config/redis/error/RedisOperationException.java index fce404ed..e99ee912 100644 --- a/src/main/java/poomasi/global/redis/error/RedisOperationException.java +++ b/src/main/java/poomasi/global/config/redis/error/RedisOperationException.java @@ -1,11 +1,7 @@ -package poomasi.global.redis.error; +package poomasi.global.config.redis.error; public class RedisOperationException extends RuntimeException { public RedisOperationException(String message) { super(message); } - - public RedisOperationException(String message, Throwable cause) { - super(message, cause); - } } \ No newline at end of file diff --git a/src/main/java/poomasi/global/redis/service/RedisService.java b/src/main/java/poomasi/global/redis/service/RedisService.java deleted file mode 100644 index 79ceec4f..00000000 --- a/src/main/java/poomasi/global/redis/service/RedisService.java +++ /dev/null @@ -1,110 +0,0 @@ -package poomasi.global.redis.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.Cursor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ScanOptions; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.stereotype.Component; -import poomasi.global.redis.error.RedisConnectionException; -import poomasi.global.redis.error.RedisOperationException; - -import java.time.Duration; -import java.util.*; -import java.util.function.Supplier; - -@Slf4j -@Component -@RequiredArgsConstructor -public class RedisService { - private final RedisTemplate redisTemplate; - private final RedisTemplate redisBlackListTemplate; - private final RedisConnectionFactory redisConnectionFactory; - - public void setValues(String key, String data, Duration duration) { - handleRedisException(() -> { - ValueOperations values = redisTemplate.opsForValue(); - values.set(key, data, duration); - return null; - }, "Redis에 값을 설정하는 중 오류 발생: " + key); - } - - public Optional getValues(String key) { - return handleRedisException(() -> { - ValueOperations values = redisTemplate.opsForValue(); - Object result = values.get(key); - return Optional.ofNullable(result).map(Object::toString); - }, "Redis에서 값을 가져오는 중 오류 발생: " + key); - } - - public void deleteValues(String key) { - handleRedisException(() -> redisTemplate.delete(key), "Redis에서 값을 삭제하는 중 오류 발생: " + key); - } - - public boolean hasKey(String key) { - return handleRedisException(() -> Boolean.TRUE.equals(redisTemplate.hasKey(key)), "Redis에서 키 존재 여부 확인 중 오류 발생: " + key); - } - - public void setBlackList(String key, String data, Duration duration) { - handleRedisException(() -> { - ValueOperations values = redisBlackListTemplate.opsForValue(); - values.set(key, data, duration); - return null; - }, "블랙리스트에 값을 설정하는 중 오류 발생: " + key); - } - - public Optional getBlackList(String key) { - return handleRedisException(() -> { - ValueOperations values = redisBlackListTemplate.opsForValue(); - Object result = values.get(key); - return Optional.ofNullable(result).map(Object::toString); - }, "블랙리스트에서 값을 가져오는 중 오류 발생: " + key); - } - - public void deleteBlackList(String key) { - handleRedisException(() -> redisBlackListTemplate.delete(key), "블랙리스트에서 값을 삭제하는 중 오류 발생: " + key); - } - - public boolean hasKeyBlackList(String key) { - return handleRedisException(() -> Boolean.TRUE.equals(redisBlackListTemplate.hasKey(key)), "블랙리스트에서 키 존재 여부 확인 중 오류 발생: " + key); - } - - public List getKeysByPattern(String pattern) { - Set keys = redisTemplate.keys(pattern); - return keys != null ? new ArrayList<>(keys) : Collections.emptyList(); - } - - public List scanKeysByPattern(String pattern) { - return handleRedisException(() -> { - List keys = new ArrayList<>(); - ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build(); - - try (RedisConnection connection = redisConnectionFactory.getConnection()) { - Cursor cursor = connection.scan(options); - while (cursor.hasNext()) { - keys.add(new String(cursor.next())); - } - } catch (Exception e) { - throw new RedisOperationException("Redis SCAN 중 오류 발생"); - } - return keys; - }, "SCAN 중 오류 발생: " + pattern); - } - - - private T handleRedisException(Supplier action, String errorMessage) { - try { - return action.get(); - } catch (RedisConnectionException e) { - log.error(errorMessage, e); - throw new RedisConnectionException(errorMessage); - } catch (Exception e) { - log.error(errorMessage, e); - throw new RedisOperationException(errorMessage); - } - } - -} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 52e5e49d..2e299032 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,3 +3,11 @@ spring: name: team10-poomasi config: import: optional:application-secret.yml + token: + storage: #리프레시 토큰 저장소 + type: jpa + #type: redis + blacklist: #블랙리스트 저장소 + type: jpa + #type: redis + From 71d656e89be36f3004b55a0ff9671ef5da433998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=EB=AF=BC?= <108014449+stopmin@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:25:11 +0900 Subject: [PATCH 15/17] =?UTF-8?q?=EC=98=88=EC=95=BD=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4=20=20?= =?UTF-8?q?(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Reservation 생성 - Farm - Member - FarmSchedule #32 * refactor: Farm 도메인 일부 분리 #32 * feat: Reservation Service 구현 - get...메소드들 - 예약 생성 인터페이스 #32 * feat: 농장 예약 기능 구현 - 농장 예약 - 열린 농장인가? - 유효한 농장인가? - 유효한 스케쥴인가? #31 * feat: 농장 조회 - 관리자나 자신의 농장만 조회 가능하도록 수정 #31 * feat: 농장 조회 - 관리자나 자신의 농장만 조회 가능하도록 수정 #31 * feat: 예약 취소 - 방문 3일 전까지 예약 취소 가능 #31 * fix: ADMIN은 가능하도록 수정 #31 * feature: 예약 조회 #31 --- .../farm/_schedule/entity/FarmSchedule.java | 7 +- .../service/FarmScheduleService.java | 25 +++- .../farm/controller/FarmController.java | 8 +- .../farm/service/FarmPlatformService.java | 25 ++++ .../domain/farm/service/FarmService.java | 17 ++- .../poomasi/domain/member/entity/Member.java | 6 +- .../domain/member/service/MemberService.java | 5 + .../ReservationFarmerController.java | 22 ++++ .../ReservationPlatformController.java | 39 +++++++ .../dto/request/ReservationRequest.java | 30 +++++ .../dto/response/ReservationResponse.java | 19 ++++ .../reservation/entity/Reservation.java | 107 ++++++++++++++++++ .../reservation/entity/ReservationStatus.java | 10 ++ .../repository/ReservationRepository.java | 14 +++ .../service/ReservationFarmerService.java | 21 ++++ .../service/ReservationPlatformService.java | 69 +++++++++++ .../service/ReservationService.java | 48 ++++++++ .../poomasi/global/error/BusinessError.java | 15 ++- 18 files changed, 469 insertions(+), 18 deletions(-) create mode 100644 src/main/java/poomasi/domain/farm/service/FarmPlatformService.java create mode 100644 src/main/java/poomasi/domain/reservation/controller/ReservationFarmerController.java create mode 100644 src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java create mode 100644 src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java create mode 100644 src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java create mode 100644 src/main/java/poomasi/domain/reservation/entity/Reservation.java create mode 100644 src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java create mode 100644 src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java create mode 100644 src/main/java/poomasi/domain/reservation/service/ReservationFarmerService.java create mode 100644 src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java create mode 100644 src/main/java/poomasi/domain/reservation/service/ReservationService.java diff --git a/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java b/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java index 3f043335..949a343a 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java +++ b/src/main/java/poomasi/domain/farm/_schedule/entity/FarmSchedule.java @@ -1,10 +1,7 @@ package poomasi.domain.farm._schedule.entity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.hibernate.annotations.Comment; import java.time.LocalDate; @@ -24,6 +21,7 @@ public class FarmSchedule { @Comment("예약 가능 날짜") private LocalDate date; + @Setter @Comment("예약 가능 여부") @Enumerated(EnumType.STRING) private ScheduleStatus status; @@ -38,4 +36,5 @@ public FarmSchedule(Long farmId, LocalDate date, ScheduleStatus status) { public void updateStatus(ScheduleStatus status) { this.status = status; } + } diff --git a/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java b/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java index 61c8658d..dd59b7bd 100644 --- a/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java +++ b/src/main/java/poomasi/domain/farm/_schedule/service/FarmScheduleService.java @@ -6,6 +6,7 @@ import poomasi.domain.farm._schedule.dto.FarmScheduleResponse; import poomasi.domain.farm._schedule.dto.FarmScheduleUpdateRequest; import poomasi.domain.farm._schedule.entity.FarmSchedule; +import poomasi.domain.farm._schedule.entity.ScheduleStatus; import poomasi.domain.farm._schedule.repository.FarmScheduleRepository; import poomasi.global.error.BusinessException; @@ -14,8 +15,7 @@ import java.util.Set; import java.util.stream.Collectors; -import static poomasi.global.error.BusinessError.FARM_SCHEDULE_ALREADY_EXISTS; -import static poomasi.global.error.BusinessError.START_DATE_SHOULD_BE_BEFORE_END_DATE; +import static poomasi.global.error.BusinessError.*; @Service @RequiredArgsConstructor @@ -54,5 +54,26 @@ public List getFarmSchedulesByYearAndMonth(FarmScheduleReq .toList(); } + public FarmSchedule getFarmScheduleByFarmIdAndDate(Long farmId, LocalDate date) { + return farmScheduleRepository.findByFarmIdAndDate(farmId, date) + .orElseThrow(() -> new BusinessException(FARM_SCHEDULE_NOT_FOUND)); + } + + public FarmSchedule getValidFarmScheduleByFarmIdAndDate(Long farmId, LocalDate date) { + FarmSchedule farmSchedule = getFarmScheduleByFarmIdAndDate(farmId, date); + + if (farmSchedule.getStatus() == ScheduleStatus.RESERVED) { + throw new BusinessException(FARM_SCHEDULE_ALREADY_RESERVED); + } + return farmSchedule; + } + + public void updateFarmScheduleStatus(Long farmScheduleId, ScheduleStatus status) { + FarmSchedule farmSchedule = farmScheduleRepository.findById(farmScheduleId) + .orElseThrow(() -> new BusinessException(FARM_SCHEDULE_NOT_FOUND)); + + farmSchedule.setStatus(status); + farmScheduleRepository.save(farmSchedule); + } } diff --git a/src/main/java/poomasi/domain/farm/controller/FarmController.java b/src/main/java/poomasi/domain/farm/controller/FarmController.java index cd2899f2..cb3c81af 100644 --- a/src/main/java/poomasi/domain/farm/controller/FarmController.java +++ b/src/main/java/poomasi/domain/farm/controller/FarmController.java @@ -7,22 +7,22 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import poomasi.domain.farm.service.FarmService; +import poomasi.domain.farm.service.FarmPlatformService; @RestController @RequiredArgsConstructor @RequestMapping("/api/farm") public class FarmController { - private final FarmService farmService; + private final FarmPlatformService farmPlatformService; @GetMapping("/{farmId}") public ResponseEntity getFarm(@PathVariable Long farmId) { - return ResponseEntity.ok(farmService.getFarmByFarmId(farmId)); + return ResponseEntity.ok(farmPlatformService.getFarmByFarmId(farmId)); } @GetMapping("") public ResponseEntity getFarmList(Pageable pageable) { - return ResponseEntity.ok(farmService.getFarmList(pageable)); + return ResponseEntity.ok(farmPlatformService.getFarmList(pageable)); } } diff --git a/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java b/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java new file mode 100644 index 00000000..54032ecf --- /dev/null +++ b/src/main/java/poomasi/domain/farm/service/FarmPlatformService.java @@ -0,0 +1,25 @@ +package poomasi.domain.farm.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import poomasi.domain.farm.dto.FarmResponse; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FarmPlatformService { + private final FarmService farmService; + + public FarmResponse getFarmByFarmId(Long farmId) { + return FarmResponse.fromEntity(farmService.getFarmByFarmId(farmId)); + } + + public List getFarmList(Pageable pageable) { + return farmService.getFarmList(pageable).stream() + .map(FarmResponse::fromEntity) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/poomasi/domain/farm/service/FarmService.java b/src/main/java/poomasi/domain/farm/service/FarmService.java index 8ef71096..c0c108d9 100644 --- a/src/main/java/poomasi/domain/farm/service/FarmService.java +++ b/src/main/java/poomasi/domain/farm/service/FarmService.java @@ -4,6 +4,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import poomasi.domain.farm.dto.FarmResponse; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.entity.FarmStatus; import poomasi.domain.farm.repository.FarmRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -16,16 +18,21 @@ public class FarmService { private final FarmRepository farmRepository; - public FarmResponse getFarmByFarmId(Long farmId) { + public Farm getValidFarmByFarmId(Long farmId) { + Farm farm = getFarmByFarmId(farmId); + if (farm.getStatus() != FarmStatus.OPEN) { + throw new BusinessException(BusinessError.FARM_NOT_OPEN); + } + return farm; + } + + public Farm getFarmByFarmId(Long farmId) { return farmRepository.findByIdAndDeletedAtIsNull(farmId) - .map(FarmResponse::fromEntity) .orElseThrow(() -> new BusinessException(BusinessError.FARM_NOT_FOUND)); } - - public List getFarmList(Pageable pageable) { + public List getFarmList(Pageable pageable) { return farmRepository.findByDeletedAtIsNull(pageable).stream() - .map(FarmResponse::fromEntity) .collect(Collectors.toList()); } } diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index 6db41d0c..bbf54171 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -50,7 +50,7 @@ public Member(String email, String password, LoginType loginType, Role role) { this.role = role; } - public Member(String email, Role role){ + public Member(String email, Role role) { this.email = email; this.role = role; } @@ -74,4 +74,8 @@ public Member(String email, Role role, LoginType loginType, String provideId, Me public boolean isFarmer() { return role == Role.ROLE_FARMER; } + + public boolean isAdmin() { + return role == Role.ROLE_ADMIN; + } } diff --git a/src/main/java/poomasi/domain/member/service/MemberService.java b/src/main/java/poomasi/domain/member/service/MemberService.java index 150c5d4f..53388950 100644 --- a/src/main/java/poomasi/domain/member/service/MemberService.java +++ b/src/main/java/poomasi/domain/member/service/MemberService.java @@ -39,4 +39,9 @@ public Member findMemberById(Long memberId) { return memberRepository.findByIdAndDeletedAtIsNull(memberId) .orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND)); } + + public boolean isAdmin(Long memberId) { + Member member = findMemberById(memberId); + return member.isAdmin(); + } } diff --git a/src/main/java/poomasi/domain/reservation/controller/ReservationFarmerController.java b/src/main/java/poomasi/domain/reservation/controller/ReservationFarmerController.java new file mode 100644 index 00000000..808bfa20 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/controller/ReservationFarmerController.java @@ -0,0 +1,22 @@ +package poomasi.domain.reservation.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import poomasi.domain.reservation.service.ReservationFarmerService; + +@RequestMapping("/api/v1/farmer/reservations") +@RestController +@RequiredArgsConstructor +public class ReservationFarmerController { + private final ReservationFarmerService reservationFarmerService; + + @GetMapping("") + public ResponseEntity getReservations() { + // FIXME : 임시로 farmerId를 1로 고정 + Long farmerId = 1L; + return ResponseEntity.ok(reservationFarmerService.getReservationsByFarmerId(farmerId)); + } +} diff --git a/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java b/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java new file mode 100644 index 00000000..e1fa6b3a --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/controller/ReservationPlatformController.java @@ -0,0 +1,39 @@ +package poomasi.domain.reservation.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.reservation.dto.request.ReservationRequest; +import poomasi.domain.reservation.dto.response.ReservationResponse; +import poomasi.domain.reservation.service.ReservationPlatformService; + +@RestController +@RequestMapping("/api/reservation") +@RequiredArgsConstructor +public class ReservationPlatformController { + private final ReservationPlatformService reservationPlatformService; + + @PostMapping("/create") + public ResponseEntity createReservation(@RequestBody ReservationRequest request) { + ReservationResponse reservation = reservationPlatformService.createReservation(request); + return ResponseEntity.ok(reservation); + } + + @GetMapping("/get/{reservationId}") + public ResponseEntity getReservation(@PathVariable Long reservationId) { + // FIXME: 로그인한 사용자의 ID를 가져오도록 수정 + Long memberId = 1L; + ReservationResponse reservation = reservationPlatformService.getReservation(memberId, reservationId); + return ResponseEntity.ok(reservation); + } + + @PostMapping("/cancel/{reservationId}") + public ResponseEntity cancelReservation(@PathVariable Long reservationId) { + // FIXME: 로그인한 사용자의 ID를 가져오도록 수정 + Long memberId = 1L; + + reservationPlatformService.cancelReservation(memberId, reservationId); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java new file mode 100644 index 00000000..e8e74630 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java @@ -0,0 +1,30 @@ +package poomasi.domain.reservation.dto.request; + +import lombok.Builder; +import poomasi.domain.farm._schedule.entity.FarmSchedule; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.member.entity.Member; +import poomasi.domain.reservation.entity.Reservation; + +import java.time.LocalDate; + +@Builder +public record ReservationRequest( + Long farmId, + Long memberId, + LocalDate reservationDate, + + int reservationCount, + String request +) { + public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule) { + return Reservation.builder() + .member(member) + .farm(farm) + .scheduleId(farmSchedule) + .reservationDate(reservationDate) + .reservationCount(reservationCount) + .request(request) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java b/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java new file mode 100644 index 00000000..0b397eff --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/dto/response/ReservationResponse.java @@ -0,0 +1,19 @@ +package poomasi.domain.reservation.dto.response; + +import lombok.Builder; +import poomasi.domain.reservation.entity.ReservationStatus; + +import java.time.LocalDate; + +@Builder +public record ReservationResponse( + Long farmId, + Long memberId, + Long scheduleId, + LocalDate reservationDate, + int memberCount, + ReservationStatus status, + String request + +) { +} diff --git a/src/main/java/poomasi/domain/reservation/entity/Reservation.java b/src/main/java/poomasi/domain/reservation/entity/Reservation.java new file mode 100644 index 00000000..68b47833 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/entity/Reservation.java @@ -0,0 +1,107 @@ +package poomasi.domain.reservation.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.farm._schedule.entity.FarmSchedule; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.member.entity.Member; +import poomasi.domain.reservation.dto.response.ReservationResponse; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "reservation", indexes = { + @Index(name = "idx_farm_id", columnList = "farm_id"), + @Index(name = "idx_user_id", columnList = "user_id") +}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("농장") + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "farm_id") + @Column(nullable = false) + private Farm farm; + + @Comment("예약자") + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + @Column(nullable = false) + private Member member; + + @Comment("예약 시간") + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "schedule_id") + @Column(nullable = false) + private FarmSchedule scheduleId; + + @Comment("예약 날짜") + @Column(nullable = false) + private LocalDate reservationDate; + + @Comment("예약 인원") + @Column(nullable = false) + private int memberCount; + + @Comment("예약 상태") + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReservationStatus status; + + @Comment("요청 사항") + @Column(nullable = false) + private String request; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + @Comment("예약 취소 일자") + private LocalDateTime canceledAt; + + + @Builder + public Reservation(Farm farm, Member member, FarmSchedule scheduleId, LocalDate reservationDate, int memberCount, ReservationStatus status, String request) { + this.farm = farm; + this.member = member; + this.scheduleId = scheduleId; + this.reservationDate = reservationDate; + this.memberCount = memberCount; + this.status = status; + this.request = request; + } + + public ReservationResponse toResponse() { + return ReservationResponse.builder() + .farmId(farm.getId()) + .memberId(member.getId()) + .scheduleId(scheduleId.getId()) + .reservationDate(reservationDate) + .memberCount(memberCount) + .status(status) + .request(request) + .build(); + } + + public boolean isCanceled() { + return status == ReservationStatus.CANCELED; + } + + public void cancel() { + this.status = ReservationStatus.CANCELED; + this.canceledAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java b/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java new file mode 100644 index 00000000..dc6f6e4e --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/entity/ReservationStatus.java @@ -0,0 +1,10 @@ +package poomasi.domain.reservation.entity; + +public enum ReservationStatus { + WAITING, // 대기 + ACCEPTED, // 수락 + REJECTED, // 거절 + CANCELED, // 취소 + DONE // 완료 + ; +} diff --git a/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java b/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java new file mode 100644 index 00000000..7bdbe093 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/repository/ReservationRepository.java @@ -0,0 +1,14 @@ +package poomasi.domain.reservation.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.reservation.entity.Reservation; + +import java.util.List; + +@Repository +public interface ReservationRepository extends JpaRepository { + List findAllByFarmId(Long farmId); + + List findAllByMemberId(Long memberId); +} diff --git a/src/main/java/poomasi/domain/reservation/service/ReservationFarmerService.java b/src/main/java/poomasi/domain/reservation/service/ReservationFarmerService.java new file mode 100644 index 00000000..48194840 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/service/ReservationFarmerService.java @@ -0,0 +1,21 @@ +package poomasi.domain.reservation.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.reservation.dto.response.ReservationResponse; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReservationFarmerService { + private final ReservationService reservationService; + + @Transactional(readOnly = true) + public List getReservationsByFarmerId(Long farmerId) { + return reservationService.getReservationsByFarmerId(farmerId); + } + + +} diff --git a/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java b/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java new file mode 100644 index 00000000..f78b30e7 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/service/ReservationPlatformService.java @@ -0,0 +1,69 @@ +package poomasi.domain.reservation.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.farm._schedule.entity.FarmSchedule; +import poomasi.domain.farm._schedule.entity.ScheduleStatus; +import poomasi.domain.farm._schedule.service.FarmScheduleService; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.service.FarmService; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; +import poomasi.domain.reservation.dto.request.ReservationRequest; +import poomasi.domain.reservation.dto.response.ReservationResponse; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +@RequiredArgsConstructor +public class ReservationPlatformService { + private final ReservationService reservationService; + private final MemberService memberService; + private final FarmService farmService; + private final FarmScheduleService farmScheduleService; + + private final int RESERVATION_CANCELLATION_PERIOD = 3; + + public ReservationResponse createReservation(ReservationRequest request) { + Member member = memberService.findMemberById(request.memberId()); + Farm farm = farmService.getValidFarmByFarmId(request.farmId()); + FarmSchedule farmSchedule = farmScheduleService.getValidFarmScheduleByFarmIdAndDate(request.farmId(), request.reservationDate()); + + // TODO: 예약 가능한지 확인하는 로직 추가 + + Reservation reservation = reservationService.createReservation(request.toEntity(member, farm, farmSchedule)); + + + return reservation.toResponse(); + } + + public ReservationResponse getReservation(Long memberId, Long reservationId) { + Reservation reservation = reservationService.getReservationById(reservationId); + if (!reservation.getMember().getId().equals(memberId) || !memberService.isAdmin(memberId)) { + throw new BusinessException(BusinessError.RESERVATION_NOT_ACCESSIBLE); + } + + return reservation.toResponse(); + } + + public void cancelReservation(Long memberId, Long reservationId) { + Reservation reservation = reservationService.getReservationById(reservationId); + + if (!reservation.getMember().getId().equals(memberId) || !memberService.isAdmin(memberId)) { + throw new BusinessException(BusinessError.RESERVATION_NOT_ACCESSIBLE); + } + + if (reservation.isCanceled()) { + throw new BusinessException(BusinessError.RESERVATION_ALREADY_CANCELED); + } + + // 우리 아직 예약 취소 규정 정해놓지 않았으니까 일단은 3일 전에만 취소 가능하다고 가정 + if (reservation.getReservationDate().isBefore(reservation.getReservationDate().minusDays(RESERVATION_CANCELLATION_PERIOD))) { + throw new BusinessException(BusinessError.RESERVATION_CANCELLATION_PERIOD_EXPIRED); + } + + reservationService.cancelReservation(reservation); + farmScheduleService.updateFarmScheduleStatus(reservation.getScheduleId().getId(), ScheduleStatus.PENDING); + } +} diff --git a/src/main/java/poomasi/domain/reservation/service/ReservationService.java b/src/main/java/poomasi/domain/reservation/service/ReservationService.java new file mode 100644 index 00000000..622625d7 --- /dev/null +++ b/src/main/java/poomasi/domain/reservation/service/ReservationService.java @@ -0,0 +1,48 @@ +package poomasi.domain.reservation.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.reservation.dto.response.ReservationResponse; +import poomasi.domain.reservation.entity.Reservation; +import poomasi.domain.reservation.repository.ReservationRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReservationService { + private final ReservationRepository reservationRepository; + + public Reservation getReservation(Long id) { + return reservationRepository.findById(id).orElseThrow(() -> new BusinessException(BusinessError.RESERVATION_NOT_FOUND)); + } + + public List getReservationsByMemberId(Long memberId) { + return reservationRepository.findAllByMemberId(memberId); + } + + public List getReservationsByFarmId(Long farmId) { + return reservationRepository.findAllByFarmId(farmId); + } + + public Reservation createReservation(Reservation reservation) { + return reservationRepository.save(reservation); + } + + public Reservation getReservationById(Long reservationId) { + return reservationRepository.findById(reservationId).orElseThrow(() -> new BusinessException(BusinessError.RESERVATION_NOT_FOUND)); + } + + public void cancelReservation(Reservation reservation) { + reservation.cancel(); + reservationRepository.save(reservation); + } + + public List getReservationsByFarmerId(Long farmerId) { + return reservationRepository.findAllByFarmId(farmerId).stream() + .map(Reservation::toResponse) + .toList(); + } +} diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 3d1ba525..1d6d5f40 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -23,18 +23,29 @@ public enum BusinessError { INVALID_CREDENTIAL(HttpStatus.UNAUTHORIZED, "잘못된 비밀번호 입니다."), REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "리프레시 토큰이 없습니다."), REFRESH_TOKEN_NOT_VALID(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다."), + // Farm FARM_NOT_FOUND(HttpStatus.NOT_FOUND, "농장을 찾을 수 없습니다."), FARM_OWNER_MISMATCH(HttpStatus.FORBIDDEN, "해당 농장의 소유자가 아닙니다."), FARM_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 농장이 존재합니다."), + FARM_NOT_OPEN(HttpStatus.BAD_REQUEST, "오픈되지 않은 농장입니다."), // FarmSchedule FARM_SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 날짜의 스케줄을 찾을 수 없습니다."), FARM_SCHEDULE_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 스케줄이 존재합니다."), + FARM_SCHEDULE_ALREADY_RESERVED(HttpStatus.CONFLICT, "해당 날짜에 이미 예약이 존재합니다."), + FARM_SCHEDULE_NOT_AVAILABLE(HttpStatus.BAD_REQUEST, "예약이 불가능한 날짜입니다."), + + + // Reservation + RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "예약을 찾을 수 없습니다."), + RESERVATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 예약이 존재합니다."), + RESERVATION_NOT_ACCESSIBLE(HttpStatus.FORBIDDEN, "접근할 수 없는 예약입니다."), + RESERVATION_ALREADY_CANCELED(HttpStatus.BAD_REQUEST, "이미 취소된 예약입니다."), + RESERVATION_CANCELLATION_PERIOD_EXPIRED(HttpStatus.BAD_REQUEST, "예약 취소 기간이 지났습니다."), // ETC - START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."), - ; + START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."); private final HttpStatus httpStatus; From 6a56050ed3f6aad53d2c449d343b1b24b187dd21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=EB=AF=BC?= <108014449+stopmin@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:27:54 +0900 Subject: [PATCH 16/17] =?UTF-8?q?=EC=9C=84=EC=8B=9C=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4=20(#5?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: product review 생성 조회 * feat: category - product 연관관계 추가 * feat: 카테고리 내 상품 조회 * fix: 빠진 파일 추가 * style: 아 맞다 정렬 * feat: 상품리뷰 - 상품리뷰사진 연관 관계 생성 * refactor: 평균 평점을 리뷰 추가할 때 계산하도록 변경 * feat: 충돌 해결 * feat: 수정사항 삭제 * feat: review 생성 * style: 아 맞다 정렬 * feature: 위시리스트 등록 - 재고와 무관하게 경우 위시리스트 가능하게 #57 * feature: 위시리스트 조회 #57 --------- Co-authored-by: canyos <4581974@naver.com> Co-authored-by: canyos <31244128+canyos@users.noreply.github.com> --- .../poomasi/domain/member/entity/Member.java | 6 +++ .../product/service/ProductService.java | 16 ++++++- .../WishListPlatformController.java | 33 +++++++++++++ .../wishlist/dto/WishListDeleteRequest.java | 7 +++ .../domain/wishlist/dto/WishListResponse.java | 21 ++++++++ .../dto/request/WishListAddRequest.java | 17 +++++++ .../domain/wishlist/entity/WishList.java | 48 +++++++++++++++++++ .../repository/WishListRepository.java | 12 +++++ .../service/WishListPlatformService.java | 34 +++++++++++++ .../wishlist/service/WishListService.java | 40 ++++++++++++++++ .../global/error/ApplicationException.java | 1 + .../poomasi/global/error/BusinessError.java | 5 +- .../global/error/BusinessException.java | 1 + 13 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java create mode 100644 src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java create mode 100644 src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java create mode 100644 src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java create mode 100644 src/main/java/poomasi/domain/wishlist/entity/WishList.java create mode 100644 src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java create mode 100644 src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java create mode 100644 src/main/java/poomasi/domain/wishlist/service/WishListService.java diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index bbf54171..3ca82998 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -6,7 +6,10 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; +import poomasi.domain.wishlist.entity.WishList; + import java.time.LocalDateTime; +import java.util.List; @Getter @Entity @@ -41,6 +44,9 @@ public class Member { @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private MemberProfile memberProfile; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List wishLists; + private LocalDateTime deletedAt; public Member(String email, String password, LoginType loginType, Role role) { diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java index 765a18b5..ca7b482c 100644 --- a/src/main/java/poomasi/domain/product/service/ProductService.java +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -1,9 +1,11 @@ package poomasi.domain.product.service; import java.util.List; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -22,8 +24,20 @@ public List getAllProducts() { } public ProductResponse getProductByProductId(Long productId) { + return ProductResponse.fromEntity(findProductById(productId)); + } + + + public Product findValidProductById(Long productId) { + Product product = findProductById(productId); + if (product.getStock() == 0) { + throw new BusinessException(BusinessError.PRODUCT_STOCK_ZERO); + } + return product; + } + + public Product findProductById(Long productId) { return productRepository.findByIdAndDeletedAtIsNull(productId) - .map(ProductResponse::fromEntity) .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } } diff --git a/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java b/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java new file mode 100644 index 00000000..f7b228e6 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java @@ -0,0 +1,33 @@ +package poomasi.domain.wishlist.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.wishlist.dto.WishListDeleteRequest; +import poomasi.domain.wishlist.dto.request.WishListAddRequest; +import poomasi.domain.wishlist.service.WishListPlatformService; + +@RequestMapping("/api/v1/wish-list") +@RestController +@RequiredArgsConstructor +public class WishListPlatformController { + private final WishListPlatformService wishListPlatformService; + + @PostMapping("/add") + public ResponseEntity addWishList(@RequestBody WishListAddRequest request) { + wishListPlatformService.addWishList(request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/delete") + public ResponseEntity deleteWishList(@RequestBody WishListDeleteRequest request) { + wishListPlatformService.deleteWishList(request); + return ResponseEntity.ok().build(); + } + + @GetMapping("/find") + public ResponseEntity findWishListByMemberId(@RequestBody Long memberId) { + // FIXME : memberID는 SecurityContextHolder에서 가져오도록 수정 + return ResponseEntity.ok(wishListPlatformService.findWishListByMemberId(memberId)); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java b/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java new file mode 100644 index 00000000..9651bd52 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java @@ -0,0 +1,7 @@ +package poomasi.domain.wishlist.dto; + +public record WishListDeleteRequest( + Long memberId, + Long productId +) { +} diff --git a/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java b/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java new file mode 100644 index 00000000..954b50de --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java @@ -0,0 +1,21 @@ +package poomasi.domain.wishlist.dto; + +import poomasi.domain.wishlist.entity.WishList; + +public record WishListResponse( + Long productId, + String productName, + Long price, + String imageUrl, + String description +) { + public static WishListResponse fromEntity(WishList wishList) { + return new WishListResponse( + wishList.getProduct().getId(), + wishList.getProduct().getName(), + wishList.getProduct().getPrice(), + wishList.getProduct().getImageUrl(), + wishList.getProduct().getDescription() + ); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java b/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java new file mode 100644 index 00000000..2783a879 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java @@ -0,0 +1,17 @@ +package poomasi.domain.wishlist.dto.request; + +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; +import poomasi.domain.wishlist.entity.WishList; + +public record WishListAddRequest( + Long memberId, + Long productId +) { + public WishList toEntity(Member member, Product product) { + return WishList.builder() + .member(member) + .product(product) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/entity/WishList.java b/src/main/java/poomasi/domain/wishlist/entity/WishList.java new file mode 100644 index 00000000..8b0ce31b --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/entity/WishList.java @@ -0,0 +1,48 @@ +package poomasi.domain.wishlist.entity; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CurrentTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@SQLDelete(sql = "UPDATE product SET deleted_at = current_timestamp WHERE id = ?") +public class WishList { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("회원") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Comment("상품") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Comment("등록일시") + @CurrentTimestamp + private LocalDateTime createdAt; + + @Comment("삭제일시") + private LocalDateTime deletedAt; + + @Builder + public WishList(Member member, Product product) { + this.member = member; + this.product = product; + } + +} diff --git a/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java b/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java new file mode 100644 index 00000000..1a7584b6 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java @@ -0,0 +1,12 @@ +package poomasi.domain.wishlist.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.wishlist.entity.WishList; + +import java.util.List; + +public interface WishListRepository extends JpaRepository { + List findByMemberId(Long memberId); + + void deleteByMemberIdAndProductId(Long memberId, Long productId); +} diff --git a/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java b/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java new file mode 100644 index 00000000..fc22b570 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java @@ -0,0 +1,34 @@ +package poomasi.domain.wishlist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.wishlist.dto.WishListDeleteRequest; +import poomasi.domain.wishlist.dto.WishListResponse; +import poomasi.domain.wishlist.dto.request.WishListAddRequest; +import poomasi.domain.wishlist.entity.WishList; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class WishListPlatformService { + private final WishListService wishListService; + + @Transactional + public void addWishList(WishListAddRequest request) { + wishListService.addWishList(request); + } + + @Transactional + public void deleteWishList(WishListDeleteRequest request) { + wishListService.deleteWishList(request); + } + + @Transactional(readOnly = true) + public List findWishListByMemberId(Long memberId) { + return wishListService.findWishListByMemberId(memberId).stream() + .map(WishListResponse::fromEntity) + .toList(); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/service/WishListService.java b/src/main/java/poomasi/domain/wishlist/service/WishListService.java new file mode 100644 index 00000000..a9cf39fa --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/service/WishListService.java @@ -0,0 +1,40 @@ +package poomasi.domain.wishlist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.service.ProductService; +import poomasi.domain.wishlist.dto.WishListDeleteRequest; +import poomasi.domain.wishlist.dto.request.WishListAddRequest; +import poomasi.domain.wishlist.entity.WishList; +import poomasi.domain.wishlist.repository.WishListRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class WishListService { + private final WishListRepository wishListRepository; + private final MemberService memberService; + private final ProductService productService; + + @Transactional + public void addWishList(WishListAddRequest request) { + Member member = memberService.findMemberById(request.memberId()); + Product product = productService.findProductById(request.productId()); + wishListRepository.save(request.toEntity(member, product)); + } + + @Transactional + public void deleteWishList(WishListDeleteRequest request) { + wishListRepository.deleteByMemberIdAndProductId(request.memberId(), request.productId()); + } + + @Transactional(readOnly = true) + public List findWishListByMemberId(Long memberId) { + return wishListRepository.findByMemberId(memberId); + } +} diff --git a/src/main/java/poomasi/global/error/ApplicationException.java b/src/main/java/poomasi/global/error/ApplicationException.java index f851d67c..c4f02647 100644 --- a/src/main/java/poomasi/global/error/ApplicationException.java +++ b/src/main/java/poomasi/global/error/ApplicationException.java @@ -6,5 +6,6 @@ @Getter @AllArgsConstructor public class ApplicationException extends RuntimeException { + private final ApplicationError applicationError; } diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 1d6d5f40..d9ce696a 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -10,10 +10,14 @@ public enum BusinessError { // Product PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), + PRODUCT_STOCK_ZERO(HttpStatus.BAD_REQUEST, "재고가 없습니다."), // Category CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다."), + // Review + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), + // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "중복된 이메일입니다."), @@ -47,7 +51,6 @@ public enum BusinessError { // ETC START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."); - private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/poomasi/global/error/BusinessException.java b/src/main/java/poomasi/global/error/BusinessException.java index 0bf9077b..170c7344 100644 --- a/src/main/java/poomasi/global/error/BusinessException.java +++ b/src/main/java/poomasi/global/error/BusinessException.java @@ -6,5 +6,6 @@ @AllArgsConstructor @Getter public class BusinessException extends RuntimeException { + private final BusinessError businessError; } From c82c198427f1692f99db34428e25fa749435b287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=EB=AF=BC?= <108014449+stopmin@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:27:54 +0900 Subject: [PATCH 17/17] =?UTF-8?q?=EC=9C=84=EC=8B=9C=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4=20(#5?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: product review 생성 조회 * feat: category - product 연관관계 추가 * feat: 카테고리 내 상품 조회 * fix: 빠진 파일 추가 * style: 아 맞다 정렬 * feat: 상품리뷰 - 상품리뷰사진 연관 관계 생성 * refactor: 평균 평점을 리뷰 추가할 때 계산하도록 변경 * feat: 충돌 해결 * feat: 수정사항 삭제 * feat: review 생성 * style: 아 맞다 정렬 * feature: 위시리스트 등록 - 재고와 무관하게 경우 위시리스트 가능하게 * feature: 위시리스트 조회 --------- Co-authored-by: canyos <4581974@naver.com> Co-authored-by: canyos <31244128+canyos@users.noreply.github.com> fix: JoinColumn 수정 --- .../poomasi/domain/member/entity/Member.java | 6 +++ .../product/service/ProductService.java | 16 ++++++- .../dto/request/ReservationRequest.java | 4 +- .../reservation/entity/Reservation.java | 9 ++-- .../WishListPlatformController.java | 33 +++++++++++++ .../wishlist/dto/WishListDeleteRequest.java | 7 +++ .../domain/wishlist/dto/WishListResponse.java | 21 ++++++++ .../dto/request/WishListAddRequest.java | 17 +++++++ .../domain/wishlist/entity/WishList.java | 48 +++++++++++++++++++ .../repository/WishListRepository.java | 12 +++++ .../service/WishListPlatformService.java | 34 +++++++++++++ .../wishlist/service/WishListService.java | 40 ++++++++++++++++ .../poomasi/global/error/BusinessError.java | 5 +- 13 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java create mode 100644 src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java create mode 100644 src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java create mode 100644 src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java create mode 100644 src/main/java/poomasi/domain/wishlist/entity/WishList.java create mode 100644 src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java create mode 100644 src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java create mode 100644 src/main/java/poomasi/domain/wishlist/service/WishListService.java diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index bbf54171..3ca82998 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -6,7 +6,10 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; +import poomasi.domain.wishlist.entity.WishList; + import java.time.LocalDateTime; +import java.util.List; @Getter @Entity @@ -41,6 +44,9 @@ public class Member { @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private MemberProfile memberProfile; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List wishLists; + private LocalDateTime deletedAt; public Member(String email, String password, LoginType loginType, Role role) { diff --git a/src/main/java/poomasi/domain/product/service/ProductService.java b/src/main/java/poomasi/domain/product/service/ProductService.java index 765a18b5..ca7b482c 100644 --- a/src/main/java/poomasi/domain/product/service/ProductService.java +++ b/src/main/java/poomasi/domain/product/service/ProductService.java @@ -1,9 +1,11 @@ package poomasi.domain.product.service; import java.util.List; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.product.entity.Product; import poomasi.domain.product.repository.ProductRepository; import poomasi.global.error.BusinessError; import poomasi.global.error.BusinessException; @@ -22,8 +24,20 @@ public List getAllProducts() { } public ProductResponse getProductByProductId(Long productId) { + return ProductResponse.fromEntity(findProductById(productId)); + } + + + public Product findValidProductById(Long productId) { + Product product = findProductById(productId); + if (product.getStock() == 0) { + throw new BusinessException(BusinessError.PRODUCT_STOCK_ZERO); + } + return product; + } + + public Product findProductById(Long productId) { return productRepository.findByIdAndDeletedAtIsNull(productId) - .map(ProductResponse::fromEntity) .orElseThrow(() -> new BusinessException(BusinessError.PRODUCT_NOT_FOUND)); } } diff --git a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java index e8e74630..4ffdf108 100644 --- a/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java +++ b/src/main/java/poomasi/domain/reservation/dto/request/ReservationRequest.java @@ -14,7 +14,7 @@ public record ReservationRequest( Long memberId, LocalDate reservationDate, - int reservationCount, + int memberCount, String request ) { public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule) { @@ -23,7 +23,7 @@ public Reservation toEntity(Member member, Farm farm, FarmSchedule farmSchedule) .farm(farm) .scheduleId(farmSchedule) .reservationDate(reservationDate) - .reservationCount(reservationCount) + .memberCount(memberCount) .request(request) .build(); } diff --git a/src/main/java/poomasi/domain/reservation/entity/Reservation.java b/src/main/java/poomasi/domain/reservation/entity/Reservation.java index 68b47833..87a353bb 100644 --- a/src/main/java/poomasi/domain/reservation/entity/Reservation.java +++ b/src/main/java/poomasi/domain/reservation/entity/Reservation.java @@ -30,20 +30,17 @@ public class Reservation { @Comment("농장") @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "farm_id") - @Column(nullable = false) + @JoinColumn(name = "farm_id", nullable = false) private Farm farm; @Comment("예약자") @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - @Column(nullable = false) + @JoinColumn(name = "member_id", nullable = false) private Member member; @Comment("예약 시간") @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "schedule_id") - @Column(nullable = false) + @JoinColumn(name = "schedule_id", nullable = false) private FarmSchedule scheduleId; @Comment("예약 날짜") diff --git a/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java b/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java new file mode 100644 index 00000000..f7b228e6 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/controller/WishListPlatformController.java @@ -0,0 +1,33 @@ +package poomasi.domain.wishlist.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.wishlist.dto.WishListDeleteRequest; +import poomasi.domain.wishlist.dto.request.WishListAddRequest; +import poomasi.domain.wishlist.service.WishListPlatformService; + +@RequestMapping("/api/v1/wish-list") +@RestController +@RequiredArgsConstructor +public class WishListPlatformController { + private final WishListPlatformService wishListPlatformService; + + @PostMapping("/add") + public ResponseEntity addWishList(@RequestBody WishListAddRequest request) { + wishListPlatformService.addWishList(request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/delete") + public ResponseEntity deleteWishList(@RequestBody WishListDeleteRequest request) { + wishListPlatformService.deleteWishList(request); + return ResponseEntity.ok().build(); + } + + @GetMapping("/find") + public ResponseEntity findWishListByMemberId(@RequestBody Long memberId) { + // FIXME : memberID는 SecurityContextHolder에서 가져오도록 수정 + return ResponseEntity.ok(wishListPlatformService.findWishListByMemberId(memberId)); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java b/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java new file mode 100644 index 00000000..9651bd52 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/dto/WishListDeleteRequest.java @@ -0,0 +1,7 @@ +package poomasi.domain.wishlist.dto; + +public record WishListDeleteRequest( + Long memberId, + Long productId +) { +} diff --git a/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java b/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java new file mode 100644 index 00000000..954b50de --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/dto/WishListResponse.java @@ -0,0 +1,21 @@ +package poomasi.domain.wishlist.dto; + +import poomasi.domain.wishlist.entity.WishList; + +public record WishListResponse( + Long productId, + String productName, + Long price, + String imageUrl, + String description +) { + public static WishListResponse fromEntity(WishList wishList) { + return new WishListResponse( + wishList.getProduct().getId(), + wishList.getProduct().getName(), + wishList.getProduct().getPrice(), + wishList.getProduct().getImageUrl(), + wishList.getProduct().getDescription() + ); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java b/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java new file mode 100644 index 00000000..2783a879 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/dto/request/WishListAddRequest.java @@ -0,0 +1,17 @@ +package poomasi.domain.wishlist.dto.request; + +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; +import poomasi.domain.wishlist.entity.WishList; + +public record WishListAddRequest( + Long memberId, + Long productId +) { + public WishList toEntity(Member member, Product product) { + return WishList.builder() + .member(member) + .product(product) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/entity/WishList.java b/src/main/java/poomasi/domain/wishlist/entity/WishList.java new file mode 100644 index 00000000..8b0ce31b --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/entity/WishList.java @@ -0,0 +1,48 @@ +package poomasi.domain.wishlist.entity; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CurrentTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import poomasi.domain.member.entity.Member; +import poomasi.domain.product.entity.Product; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@SQLDelete(sql = "UPDATE product SET deleted_at = current_timestamp WHERE id = ?") +public class WishList { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("회원") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Comment("상품") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Comment("등록일시") + @CurrentTimestamp + private LocalDateTime createdAt; + + @Comment("삭제일시") + private LocalDateTime deletedAt; + + @Builder + public WishList(Member member, Product product) { + this.member = member; + this.product = product; + } + +} diff --git a/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java b/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java new file mode 100644 index 00000000..1a7584b6 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/repository/WishListRepository.java @@ -0,0 +1,12 @@ +package poomasi.domain.wishlist.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.wishlist.entity.WishList; + +import java.util.List; + +public interface WishListRepository extends JpaRepository { + List findByMemberId(Long memberId); + + void deleteByMemberIdAndProductId(Long memberId, Long productId); +} diff --git a/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java b/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java new file mode 100644 index 00000000..fc22b570 --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/service/WishListPlatformService.java @@ -0,0 +1,34 @@ +package poomasi.domain.wishlist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.wishlist.dto.WishListDeleteRequest; +import poomasi.domain.wishlist.dto.WishListResponse; +import poomasi.domain.wishlist.dto.request.WishListAddRequest; +import poomasi.domain.wishlist.entity.WishList; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class WishListPlatformService { + private final WishListService wishListService; + + @Transactional + public void addWishList(WishListAddRequest request) { + wishListService.addWishList(request); + } + + @Transactional + public void deleteWishList(WishListDeleteRequest request) { + wishListService.deleteWishList(request); + } + + @Transactional(readOnly = true) + public List findWishListByMemberId(Long memberId) { + return wishListService.findWishListByMemberId(memberId).stream() + .map(WishListResponse::fromEntity) + .toList(); + } +} diff --git a/src/main/java/poomasi/domain/wishlist/service/WishListService.java b/src/main/java/poomasi/domain/wishlist/service/WishListService.java new file mode 100644 index 00000000..a9cf39fa --- /dev/null +++ b/src/main/java/poomasi/domain/wishlist/service/WishListService.java @@ -0,0 +1,40 @@ +package poomasi.domain.wishlist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.service.MemberService; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.service.ProductService; +import poomasi.domain.wishlist.dto.WishListDeleteRequest; +import poomasi.domain.wishlist.dto.request.WishListAddRequest; +import poomasi.domain.wishlist.entity.WishList; +import poomasi.domain.wishlist.repository.WishListRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class WishListService { + private final WishListRepository wishListRepository; + private final MemberService memberService; + private final ProductService productService; + + @Transactional + public void addWishList(WishListAddRequest request) { + Member member = memberService.findMemberById(request.memberId()); + Product product = productService.findProductById(request.productId()); + wishListRepository.save(request.toEntity(member, product)); + } + + @Transactional + public void deleteWishList(WishListDeleteRequest request) { + wishListRepository.deleteByMemberIdAndProductId(request.memberId(), request.productId()); + } + + @Transactional(readOnly = true) + public List findWishListByMemberId(Long memberId) { + return wishListRepository.findByMemberId(memberId); + } +} diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 38c31bf1..d9ce696a 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -10,10 +10,14 @@ public enum BusinessError { // Product PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), + PRODUCT_STOCK_ZERO(HttpStatus.BAD_REQUEST, "재고가 없습니다."), + // Category CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다."), + // Review REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), + // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "중복된 이메일입니다."), @@ -47,7 +51,6 @@ public enum BusinessError { // ETC START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."); - private final HttpStatus httpStatus; private final String message;