From e36a9c6735fa341f7e8b1dba414d5e103b9ac3b9 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 03:55:06 +0900 Subject: [PATCH 01/28] =?UTF-8?q?#27=20add:=20JpaAuditing=EC=9D=84=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20BaseTimeEntity=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/helpmeCookies/Step3Application.java | 2 ++ .../global/entity/BaseTimeEntity.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java diff --git a/src/main/java/com/helpmeCookies/Step3Application.java b/src/main/java/com/helpmeCookies/Step3Application.java index c6bad29..a594415 100644 --- a/src/main/java/com/helpmeCookies/Step3Application.java +++ b/src/main/java/com/helpmeCookies/Step3Application.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class Step3Application { public static void main(String[] args) { diff --git a/src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java b/src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java new file mode 100644 index 0000000..9279fc3 --- /dev/null +++ b/src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java @@ -0,0 +1,21 @@ +package com.helpmeCookies.global.entity; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; +} From 05c59068ae52819226879e16124cb714d10a9017 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:24:49 +0900 Subject: [PATCH 02/28] =?UTF-8?q?#27=20fix,=20add:=20ArtistInfo=20Id=20Gen?= =?UTF-8?q?erator=20=EC=88=98=EC=A0=95,=20name=20column=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/helpmeCookies/user/entity/ArtistInfo.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/helpmeCookies/user/entity/ArtistInfo.java b/src/main/java/com/helpmeCookies/user/entity/ArtistInfo.java index 83d86e7..4d74c5e 100644 --- a/src/main/java/com/helpmeCookies/user/entity/ArtistInfo.java +++ b/src/main/java/com/helpmeCookies/user/entity/ArtistInfo.java @@ -5,18 +5,23 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; @Entity public class ArtistInfo { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotNull + private String name; + @Column(nullable = false) private Long totalFollowers; @Column(nullable = false) private Long totalLikes; + private String about; } From 3a3751b5b1d8f3d824dd23739af2b02dffa266b9 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:25:27 +0900 Subject: [PATCH 03/28] =?UTF-8?q?#27=20add:=20Product=20BaseTimeEntity=20?= =?UTF-8?q?=EC=83=81=EC=86=8D,=20ArtistInfo=20JoinColumn=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/helpmeCookies/product/entity/Product.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/helpmeCookies/product/entity/Product.java b/src/main/java/com/helpmeCookies/product/entity/Product.java index d44b972..584b32a 100644 --- a/src/main/java/com/helpmeCookies/product/entity/Product.java +++ b/src/main/java/com/helpmeCookies/product/entity/Product.java @@ -1,5 +1,7 @@ package com.helpmeCookies.product.entity; +import com.helpmeCookies.global.entity.BaseTimeEntity; +import jakarta.persistence.JoinColumn; import java.util.List; import com.helpmeCookies.user.entity.ArtistInfo; @@ -17,7 +19,7 @@ import lombok.Builder; @Entity -public class Product { +public class Product extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -48,6 +50,7 @@ public class Product { private List hashTags; @ManyToOne + @JoinColumn(name = "artist_info_id") private ArtistInfo artistInfo; public Product() {} From 2c9e4999b88899a0c79874268e80e334e544a85e Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:26:55 +0900 Subject: [PATCH 04/28] =?UTF-8?q?#27=20feat(persistence):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1(imageUrl=20=EC=B6=94=EA=B0=80=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/repository/ProductRepository.java | 19 +++++++++++++++++++ .../product/repository/dto/ProductSearch.java | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java diff --git a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java index d8b05f3..aca10a1 100644 --- a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java +++ b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java @@ -1,9 +1,28 @@ package com.helpmeCookies.product.repository; import com.helpmeCookies.product.entity.Product; +import com.helpmeCookies.product.repository.dto.ProductSearch; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface ProductRepository extends JpaRepository { + + @Query("SELECT p.id AS id, p.name AS name, a.name AS artist, p.price AS price " + + "FROM Product p JOIN p.artistInfo a " + + "WHERE p.name LIKE %:query%") // Index 미사용 + Page findByName(@Param("query") String query, Pageable pageable); + + @Query(value = "SELECT p.id, p.name, a.name AS artist, p.price " + + "FROM product p JOIN artist_info a ON p.artist_info_id = a.id " + + "WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)", + countQuery = "SELECT COUNT(*) " + + "FROM product p JOIN artist_info a ON p.artist_info_id = a.id " + + "WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)", + nativeQuery = true) // Index 사용 + Page findByNameWithIdx(@Param("query") String query, Pageable pageable); } diff --git a/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java new file mode 100644 index 0000000..3c5fcee --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java @@ -0,0 +1,9 @@ +package com.helpmeCookies.product.repository.dto; + + +public interface ProductSearch { + Long getId(); + String getName(); + String getArtist(); + Long getPrice(); +} From e5c750e646c28556721c50b260b281c1869ba92e Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:27:10 +0900 Subject: [PATCH 05/28] =?UTF-8?q?#27=20feat(service):=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?(imageUrl=20=EC=B6=94=EA=B0=80=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ProductService.java | 11 ++++- .../product/service/dto/ProductPage.java | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java diff --git a/src/main/java/com/helpmeCookies/product/service/ProductService.java b/src/main/java/com/helpmeCookies/product/service/ProductService.java index 1a16699..6b3c443 100644 --- a/src/main/java/com/helpmeCookies/product/service/ProductService.java +++ b/src/main/java/com/helpmeCookies/product/service/ProductService.java @@ -4,15 +4,22 @@ import com.helpmeCookies.product.entity.Category; import com.helpmeCookies.product.entity.Product; import com.helpmeCookies.product.repository.ProductRepository; +import com.helpmeCookies.product.service.dto.ProductPage; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class ProductService { + private final ProductRepository productRepository; - public ProductService(ProductRepository productRepository) { - this.productRepository = productRepository; + @Transactional(readOnly = true) + public ProductPage.Paging getProductsByPage(String query, Pageable pageable) { + var productPage = productRepository.findByNameWithIdx(query, pageable); + return ProductPage.Paging.from(productPage); } public Product save(ProductRequest productSaveRequest) { diff --git a/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java b/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java new file mode 100644 index 0000000..0b2b920 --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java @@ -0,0 +1,44 @@ +package com.helpmeCookies.product.service.dto; + +import com.helpmeCookies.product.repository.dto.ProductSearch; +import java.util.List; +import org.springframework.data.domain.Page; + +public class ProductPage { + + public record Info( + Long id, + String name, + String artist, + Long price + ) { + public static Info from(ProductSearch productSearch) { + return new Info( + productSearch.getId(), + productSearch.getName(), + productSearch.getArtist(), + productSearch.getPrice() + ); + } + + public static List of(List content) { + return content.stream() + .map(Info::from) + .toList(); + } + } + + public record Paging ( + boolean hasNext, + List products + ) { + + public static Paging from(Page productPage) { + return new Paging( + productPage.hasNext(), + Info.of(productPage.getContent()) + ); + } + } + +} From fd66d6d279cd72e41345a260e7ee65fcbe9a8e36 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:27:18 +0900 Subject: [PATCH 06/28] =?UTF-8?q?#27=20feat(presentation):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1(imageUrl=20=EC=B6=94=EA=B0=80=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 36 +++++++++++++++---- .../product/util/ProductSort.java | 5 +++ .../helpmeCookies/product/util/SortUtil.java | 14 ++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/helpmeCookies/product/util/ProductSort.java create mode 100644 src/main/java/com/helpmeCookies/product/util/SortUtil.java diff --git a/src/main/java/com/helpmeCookies/product/controller/ProductController.java b/src/main/java/com/helpmeCookies/product/controller/ProductController.java index c87732e..884a774 100644 --- a/src/main/java/com/helpmeCookies/product/controller/ProductController.java +++ b/src/main/java/com/helpmeCookies/product/controller/ProductController.java @@ -1,22 +1,33 @@ package com.helpmeCookies.product.controller; +import static com.helpmeCookies.product.util.SortUtil.convertProductSort; + import com.helpmeCookies.product.dto.ProductRequest; import com.helpmeCookies.product.dto.ProductResponse; import com.helpmeCookies.product.entity.Product; import com.helpmeCookies.product.service.ProductService; +import com.helpmeCookies.product.service.dto.ProductPage; +import com.helpmeCookies.product.util.ProductSort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/products") +@RequestMapping("/v1/products") +@RequiredArgsConstructor public class ProductController { private final ProductService productService; - public ProductController(ProductService productService) { - this.productService = productService; - } - @PostMapping public ResponseEntity saveProduct(@RequestBody ProductRequest productRequest) { Product product = productService.save(productRequest); @@ -41,4 +52,17 @@ public ResponseEntity deleteProduct(@PathVariable("productId") Long produc productService.delete(productId); return ResponseEntity.noContent().build(); } + + @GetMapping + public ResponseEntity getProductsByPage( + @RequestParam("query") String query, + @RequestParam(name = "size", required = false, defaultValue = "20") int size, + @RequestParam("page") int page, + @RequestParam("sort") ProductSort productSort + ) { + var sort = convertProductSort(productSort); + var pageable = PageRequest.of(page, size, sort); + + return ResponseEntity.ok(productService.getProductsByPage(query, pageable)); + } } diff --git a/src/main/java/com/helpmeCookies/product/util/ProductSort.java b/src/main/java/com/helpmeCookies/product/util/ProductSort.java new file mode 100644 index 0000000..1a37cec --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/util/ProductSort.java @@ -0,0 +1,5 @@ +package com.helpmeCookies.product.util; + +public enum ProductSort { + RELEVANCE, LATEST +} diff --git a/src/main/java/com/helpmeCookies/product/util/SortUtil.java b/src/main/java/com/helpmeCookies/product/util/SortUtil.java new file mode 100644 index 0000000..3186acf --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/util/SortUtil.java @@ -0,0 +1,14 @@ +package com.helpmeCookies.product.util; + +import org.springframework.data.domain.Sort; + +public class SortUtil { + + public static Sort convertProductSort(ProductSort productSort) { + return switch (productSort) { + case LATEST -> Sort.by(Sort.Order.desc("createdDate")); + default -> Sort.unsorted(); + }; + } + +} From 73f68e5339696e090ca9a195040e8c05cbdec1c6 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 03:55:06 +0900 Subject: [PATCH 07/28] =?UTF-8?q?#27=20add:=20JpaAuditing=EC=9D=84=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20BaseTimeEntity=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/helpmeCookies/Step3Application.java | 2 ++ .../global/entity/BaseTimeEntity.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java diff --git a/src/main/java/com/helpmeCookies/Step3Application.java b/src/main/java/com/helpmeCookies/Step3Application.java index c6bad29..a594415 100644 --- a/src/main/java/com/helpmeCookies/Step3Application.java +++ b/src/main/java/com/helpmeCookies/Step3Application.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class Step3Application { public static void main(String[] args) { diff --git a/src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java b/src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java new file mode 100644 index 0000000..9279fc3 --- /dev/null +++ b/src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java @@ -0,0 +1,21 @@ +package com.helpmeCookies.global.entity; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; +} From b753e67e8a9fe3d96371fd7892c11e68a1a1d620 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:25:27 +0900 Subject: [PATCH 08/28] =?UTF-8?q?#27=20add:=20Product=20BaseTimeEntity=20?= =?UTF-8?q?=EC=83=81=EC=86=8D,=20ArtistInfo=20JoinColumn=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/helpmeCookies/product/entity/Product.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/helpmeCookies/product/entity/Product.java b/src/main/java/com/helpmeCookies/product/entity/Product.java index d44b972..584b32a 100644 --- a/src/main/java/com/helpmeCookies/product/entity/Product.java +++ b/src/main/java/com/helpmeCookies/product/entity/Product.java @@ -1,5 +1,7 @@ package com.helpmeCookies.product.entity; +import com.helpmeCookies.global.entity.BaseTimeEntity; +import jakarta.persistence.JoinColumn; import java.util.List; import com.helpmeCookies.user.entity.ArtistInfo; @@ -17,7 +19,7 @@ import lombok.Builder; @Entity -public class Product { +public class Product extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -48,6 +50,7 @@ public class Product { private List hashTags; @ManyToOne + @JoinColumn(name = "artist_info_id") private ArtistInfo artistInfo; public Product() {} From 2c29a01bbb1c5cc5e5ea143623aa929549f5e3c4 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:26:55 +0900 Subject: [PATCH 09/28] =?UTF-8?q?#27=20feat(persistence):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1(imageUrl=20=EC=B6=94=EA=B0=80=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/repository/ProductRepository.java | 19 +++++++++++++++++++ .../product/repository/dto/ProductSearch.java | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java diff --git a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java index d8b05f3..aca10a1 100644 --- a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java +++ b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java @@ -1,9 +1,28 @@ package com.helpmeCookies.product.repository; import com.helpmeCookies.product.entity.Product; +import com.helpmeCookies.product.repository.dto.ProductSearch; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface ProductRepository extends JpaRepository { + + @Query("SELECT p.id AS id, p.name AS name, a.name AS artist, p.price AS price " + + "FROM Product p JOIN p.artistInfo a " + + "WHERE p.name LIKE %:query%") // Index 미사용 + Page findByName(@Param("query") String query, Pageable pageable); + + @Query(value = "SELECT p.id, p.name, a.name AS artist, p.price " + + "FROM product p JOIN artist_info a ON p.artist_info_id = a.id " + + "WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)", + countQuery = "SELECT COUNT(*) " + + "FROM product p JOIN artist_info a ON p.artist_info_id = a.id " + + "WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)", + nativeQuery = true) // Index 사용 + Page findByNameWithIdx(@Param("query") String query, Pageable pageable); } diff --git a/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java new file mode 100644 index 0000000..3c5fcee --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java @@ -0,0 +1,9 @@ +package com.helpmeCookies.product.repository.dto; + + +public interface ProductSearch { + Long getId(); + String getName(); + String getArtist(); + Long getPrice(); +} From 1d2d527c91d46d197ed72895ec0aade02c99f06e Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:27:10 +0900 Subject: [PATCH 10/28] =?UTF-8?q?#27=20feat(product.service):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ProductService.java | 8 ++++ .../product/service/dto/ProductPage.java | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java diff --git a/src/main/java/com/helpmeCookies/product/service/ProductService.java b/src/main/java/com/helpmeCookies/product/service/ProductService.java index c9b22d4..b0b156d 100644 --- a/src/main/java/com/helpmeCookies/product/service/ProductService.java +++ b/src/main/java/com/helpmeCookies/product/service/ProductService.java @@ -5,7 +5,9 @@ import com.helpmeCookies.product.entity.Product; import com.helpmeCookies.product.repository.ProductImageRepository; import com.helpmeCookies.product.repository.ProductRepository; +import com.helpmeCookies.product.dto.ProductPage; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +17,12 @@ public class ProductService { private final ProductRepository productRepository; private final ProductImageRepository productImageRepository; + @Transactional(readOnly = true) + public ProductPage.Paging getProductsByPage(String query, Pageable pageable) { + var productPage = productRepository.findByNameWithIdx(query, pageable); + return ProductPage.Paging.from(productPage); + } + public Product save(ProductRequest productSaveRequest) { //TODO ArtistInfo 코드 병합시 수정 예정 Product product = productSaveRequest.toEntity(null); diff --git a/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java b/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java new file mode 100644 index 0000000..0b2b920 --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java @@ -0,0 +1,44 @@ +package com.helpmeCookies.product.service.dto; + +import com.helpmeCookies.product.repository.dto.ProductSearch; +import java.util.List; +import org.springframework.data.domain.Page; + +public class ProductPage { + + public record Info( + Long id, + String name, + String artist, + Long price + ) { + public static Info from(ProductSearch productSearch) { + return new Info( + productSearch.getId(), + productSearch.getName(), + productSearch.getArtist(), + productSearch.getPrice() + ); + } + + public static List of(List content) { + return content.stream() + .map(Info::from) + .toList(); + } + } + + public record Paging ( + boolean hasNext, + List products + ) { + + public static Paging from(Page productPage) { + return new Paging( + productPage.hasNext(), + Info.of(productPage.getContent()) + ); + } + } + +} From e4e393ae1b922e8a1b482974efed075ce78728a3 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Thu, 3 Oct 2024 04:27:18 +0900 Subject: [PATCH 11/28] =?UTF-8?q?#27=20feat(product.controller):=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 18 ++++++++++++++++++ .../product/util/ProductSort.java | 5 +++++ .../helpmeCookies/product/util/SortUtil.java | 14 ++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/product/util/ProductSort.java create mode 100644 src/main/java/com/helpmeCookies/product/util/SortUtil.java diff --git a/src/main/java/com/helpmeCookies/product/controller/ProductController.java b/src/main/java/com/helpmeCookies/product/controller/ProductController.java index c655ee7..e3b03c9 100644 --- a/src/main/java/com/helpmeCookies/product/controller/ProductController.java +++ b/src/main/java/com/helpmeCookies/product/controller/ProductController.java @@ -2,12 +2,17 @@ import com.helpmeCookies.product.dto.FileUploadResponse; import com.helpmeCookies.product.dto.ProductImageResponse; +import static com.helpmeCookies.product.util.SortUtil.convertProductSort; + import com.helpmeCookies.product.dto.ProductRequest; import com.helpmeCookies.product.dto.ProductResponse; import com.helpmeCookies.product.entity.Product; import com.helpmeCookies.product.service.ProductImageService; import com.helpmeCookies.product.service.ProductService; import lombok.RequiredArgsConstructor; +import com.helpmeCookies.product.dto.ProductPage; +import com.helpmeCookies.product.util.ProductSort; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -59,4 +64,17 @@ public ResponseEntity deleteProduct(@PathVariable("productId") Long produc productService.delete(productId); return ResponseEntity.noContent().build(); } + + @GetMapping + public ResponseEntity getProductsByPage( + @RequestParam("query") String query, + @RequestParam(name = "size", required = false, defaultValue = "20") int size, + @RequestParam("page") int page, + @RequestParam("sort") ProductSort productSort + ) { + var sort = convertProductSort(productSort); + var pageable = PageRequest.of(page, size, sort); + + return ResponseEntity.ok(productService.getProductsByPage(query, pageable)); + } } diff --git a/src/main/java/com/helpmeCookies/product/util/ProductSort.java b/src/main/java/com/helpmeCookies/product/util/ProductSort.java new file mode 100644 index 0000000..1a37cec --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/util/ProductSort.java @@ -0,0 +1,5 @@ +package com.helpmeCookies.product.util; + +public enum ProductSort { + RELEVANCE, LATEST +} diff --git a/src/main/java/com/helpmeCookies/product/util/SortUtil.java b/src/main/java/com/helpmeCookies/product/util/SortUtil.java new file mode 100644 index 0000000..3186acf --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/util/SortUtil.java @@ -0,0 +1,14 @@ +package com.helpmeCookies.product.util; + +import org.springframework.data.domain.Sort; + +public class SortUtil { + + public static Sort convertProductSort(ProductSort productSort) { + return switch (productSort) { + case LATEST -> Sort.by(Sort.Order.desc("createdDate")); + default -> Sort.unsorted(); + }; + } + +} From e2456f8b01f7d55fea5dac9cd20d518fbcbc585c Mon Sep 17 00:00:00 2001 From: sim-mer Date: Fri, 25 Oct 2024 04:16:51 +0900 Subject: [PATCH 12/28] =?UTF-8?q?#27=20chore(build.gradle):=20testcontaine?= =?UTF-8?q?rs=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 2000e5e..615d23c 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,14 @@ dependencies { //S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + //Test + testImplementation 'org.testcontainers:testcontainers:1.20.2' + testImplementation 'org.testcontainers:junit-jupiter:1.20.2' + testImplementation 'org.testcontainers:mysql:1.20.2' + } + tasks.named('test') { useJUnitPlatform() } From 46e885e9fb8e33afa6a4c860f726d53750a7a8e9 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Fri, 25 Oct 2024 04:19:44 +0900 Subject: [PATCH 13/28] =?UTF-8?q?#27=20refactor(ProductPage.java):=20Produ?= =?UTF-8?q?ctPage=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 3 +- .../product/service/dto/ProductPage.java | 44 ------------------- 2 files changed, 2 insertions(+), 45 deletions(-) delete mode 100644 src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java diff --git a/src/main/java/com/helpmeCookies/product/controller/ProductController.java b/src/main/java/com/helpmeCookies/product/controller/ProductController.java index e3b03c9..f87b105 100644 --- a/src/main/java/com/helpmeCookies/product/controller/ProductController.java +++ b/src/main/java/com/helpmeCookies/product/controller/ProductController.java @@ -4,14 +4,15 @@ import com.helpmeCookies.product.dto.ProductImageResponse; import static com.helpmeCookies.product.util.SortUtil.convertProductSort; +import com.helpmeCookies.product.dto.ProductPage; import com.helpmeCookies.product.dto.ProductRequest; import com.helpmeCookies.product.dto.ProductResponse; import com.helpmeCookies.product.entity.Product; import com.helpmeCookies.product.service.ProductImageService; import com.helpmeCookies.product.service.ProductService; +import com.helpmeCookies.product.util.ProductSort; import lombok.RequiredArgsConstructor; import com.helpmeCookies.product.dto.ProductPage; -import com.helpmeCookies.product.util.ProductSort; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java b/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java deleted file mode 100644 index 0b2b920..0000000 --- a/src/main/java/com/helpmeCookies/product/service/dto/ProductPage.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.helpmeCookies.product.service.dto; - -import com.helpmeCookies.product.repository.dto.ProductSearch; -import java.util.List; -import org.springframework.data.domain.Page; - -public class ProductPage { - - public record Info( - Long id, - String name, - String artist, - Long price - ) { - public static Info from(ProductSearch productSearch) { - return new Info( - productSearch.getId(), - productSearch.getName(), - productSearch.getArtist(), - productSearch.getPrice() - ); - } - - public static List of(List content) { - return content.stream() - .map(Info::from) - .toList(); - } - } - - public record Paging ( - boolean hasNext, - List products - ) { - - public static Paging from(Page productPage) { - return new Paging( - productPage.hasNext(), - Info.of(productPage.getContent()) - ); - } - } - -} From be44e0fa2211c205418e3fedd019baa959134b1f Mon Sep 17 00:00:00 2001 From: sim-mer Date: Fri, 25 Oct 2024 04:21:08 +0900 Subject: [PATCH 14/28] =?UTF-8?q?#27=20fix(SortUtil.java):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EC=A0=95=EB=A0=AC=20=EB=B3=80=ED=99=98=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8A=A4=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=ED=81=AC=20=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/helpmeCookies/product/util/SortUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/helpmeCookies/product/util/SortUtil.java b/src/main/java/com/helpmeCookies/product/util/SortUtil.java index 3186acf..e85e068 100644 --- a/src/main/java/com/helpmeCookies/product/util/SortUtil.java +++ b/src/main/java/com/helpmeCookies/product/util/SortUtil.java @@ -6,7 +6,7 @@ public class SortUtil { public static Sort convertProductSort(ProductSort productSort) { return switch (productSort) { - case LATEST -> Sort.by(Sort.Order.desc("createdDate")); + case LATEST -> Sort.by(Sort.Order.desc("created_date")); default -> Sort.unsorted(); }; } From 8f56e6a6f85bb824ee095625a2fe4bb4e44e8a16 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Fri, 25 Oct 2024 04:32:47 +0900 Subject: [PATCH 15/28] =?UTF-8?q?#27=20feat(product):=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=95=20DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/dto/ProductPage.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/product/dto/ProductPage.java diff --git a/src/main/java/com/helpmeCookies/product/dto/ProductPage.java b/src/main/java/com/helpmeCookies/product/dto/ProductPage.java new file mode 100644 index 0000000..1648c19 --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/dto/ProductPage.java @@ -0,0 +1,44 @@ +package com.helpmeCookies.product.dto; + +import com.helpmeCookies.product.repository.dto.ProductSearch; +import java.util.List; +import org.springframework.data.domain.Page; + +public class ProductPage { + + public record Info( + Long id, + String name, + String artist, + Long price + ) { + public static Info from(ProductSearch productSearch) { + return new Info( + productSearch.getId(), + productSearch.getName(), + productSearch.getArtist(), + productSearch.getPrice() + ); + } + + public static List of(List content) { + return content.stream() + .map(Info::from) + .toList(); + } + } + + public record Paging ( + boolean hasNext, + List products + ) { + + public static Paging from(Page productPage) { + return new Paging( + productPage.hasNext(), + Info.of(productPage.getContent()) + ); + } + } + +} From 36bd84f82e8906af013e264eb91889983c2f1e90 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Sun, 27 Oct 2024 10:20:08 +0900 Subject: [PATCH 16/28] =?UTF-8?q?#27=20docs(product):=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20api=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?url=20mapping=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 10 ++++----- .../controller/docs/ProductApiDocs.java | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/helpmeCookies/product/controller/docs/ProductApiDocs.java diff --git a/src/main/java/com/helpmeCookies/product/controller/ProductController.java b/src/main/java/com/helpmeCookies/product/controller/ProductController.java index f87b105..e575c09 100644 --- a/src/main/java/com/helpmeCookies/product/controller/ProductController.java +++ b/src/main/java/com/helpmeCookies/product/controller/ProductController.java @@ -1,9 +1,10 @@ package com.helpmeCookies.product.controller; -import com.helpmeCookies.product.dto.FileUploadResponse; -import com.helpmeCookies.product.dto.ProductImageResponse; import static com.helpmeCookies.product.util.SortUtil.convertProductSort; +import com.helpmeCookies.product.controller.docs.ProductApiDocs; +import com.helpmeCookies.product.dto.FileUploadResponse; +import com.helpmeCookies.product.dto.ProductImageResponse; import com.helpmeCookies.product.dto.ProductPage; import com.helpmeCookies.product.dto.ProductRequest; import com.helpmeCookies.product.dto.ProductResponse; @@ -12,7 +13,6 @@ import com.helpmeCookies.product.service.ProductService; import com.helpmeCookies.product.util.ProductSort; import lombok.RequiredArgsConstructor; -import com.helpmeCookies.product.dto.ProductPage; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -22,9 +22,9 @@ import java.util.List; @RestController -@RequestMapping("/api/v1/products") +@RequestMapping("/v1/products") @RequiredArgsConstructor -public class ProductController { +public class ProductController implements ProductApiDocs { private final ProductService productService; private final ProductImageService productImageService; diff --git a/src/main/java/com/helpmeCookies/product/controller/docs/ProductApiDocs.java b/src/main/java/com/helpmeCookies/product/controller/docs/ProductApiDocs.java new file mode 100644 index 0000000..e0097b5 --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/controller/docs/ProductApiDocs.java @@ -0,0 +1,21 @@ +package com.helpmeCookies.product.controller.docs; + +import com.helpmeCookies.product.dto.ProductPage.Paging; +import com.helpmeCookies.product.util.ProductSort; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "상품 관련 기능", description = "상품 관련 API") +public interface ProductApiDocs { + + @Operation(summary = "상품 검색") + ResponseEntity getProductsByPage( + String query, + @Parameter(description = "default value 20") int size, + int page, + ProductSort productSort + ); + +} From 30769a7bae59e862cc0d219b72604cc543922270 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Sun, 27 Oct 2024 10:20:54 +0900 Subject: [PATCH 17/28] =?UTF-8?q?#27=20fix(product):=20ArtistInfo=20entity?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helpmeCookies/product/repository/ProductRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java index aca10a1..512722d 100644 --- a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java +++ b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java @@ -12,12 +12,12 @@ @Repository public interface ProductRepository extends JpaRepository { - @Query("SELECT p.id AS id, p.name AS name, a.name AS artist, p.price AS price " + + @Query("SELECT p.id AS id, p.name AS name, a.nickname AS artist, p.price AS price " + "FROM Product p JOIN p.artistInfo a " + "WHERE p.name LIKE %:query%") // Index 미사용 Page findByName(@Param("query") String query, Pageable pageable); - @Query(value = "SELECT p.id, p.name, a.name AS artist, p.price " + + @Query(value = "SELECT p.id, p.name, a.nickname AS artist, p.price " + "FROM product p JOIN artist_info a ON p.artist_info_id = a.id " + "WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)", countQuery = "SELECT COUNT(*) " + From 803d9dfb773b5a07b1abc0d23fdd297161a202e7 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Sun, 27 Oct 2024 10:22:37 +0900 Subject: [PATCH 18/28] =?UTF-8?q?#27=20test(product):=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/search/SearchControllerTest.java | 76 +++++++++++++++++++ .../product/search/SearchRepositoryTest.java | 54 +++++++++++++ .../product/search/SearchServiceTest.java | 63 +++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java create mode 100644 src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java create mode 100644 src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java diff --git a/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java b/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java new file mode 100644 index 0000000..be84884 --- /dev/null +++ b/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java @@ -0,0 +1,76 @@ +package com.helpmeCookies.product.search; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.helpmeCookies.global.jwt.JwtProvider; +import com.helpmeCookies.product.controller.ProductController; +import com.helpmeCookies.product.dto.ProductPage; +import com.helpmeCookies.product.repository.dto.ProductSearch; +import com.helpmeCookies.product.service.ProductImageService; +import com.helpmeCookies.product.service.ProductService; +import com.helpmeCookies.product.util.ProductSort; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ProductController.class) +@AutoConfigureMockMvc(addFilters = false) +@Import(JwtProvider.class) +public class SearchControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private ProductService productService; + + @MockBean + private ProductImageService productImageService; + + + @Test + @DisplayName("상품 검색 컨트롤러") + void 상품_검색() throws Exception { + // given + String query = "product"; + int size = 10; + int page = 0; + ProductSort productSort = ProductSort.LATEST; + + var productSearch = mock(ProductSearch.class); + given(productSearch.getId()).willReturn(1L); + given(productSearch.getName()).willReturn("product1"); + given(productSearch.getArtist()).willReturn("artist"); + given(productSearch.getPrice()).willReturn(10000L); + var paging = ProductPage.Paging.from(new PageImpl<>(List.of(productSearch))); + given(productService.getProductsByPage(eq(query), any(Pageable.class))) + .willReturn(paging); + + // when & then + mvc.perform(get("/test/v1/products") + .param("query", query) + .param("size", String.valueOf(size)) + .param("page", String.valueOf(page)) + .param("sort", productSort.name())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.hasNext").value(false)) + .andExpect(jsonPath("$.products[0].name").value("product1")) + .andExpect(jsonPath("$.products[0].artist").value("artist")) + .andExpect(jsonPath("$.products[0].price").value(10000L)); + } + +} diff --git a/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java b/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java new file mode 100644 index 0000000..b5f2ad0 --- /dev/null +++ b/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java @@ -0,0 +1,54 @@ +package com.helpmeCookies.product.search; + +import static com.helpmeCookies.product.util.SortUtil.convertProductSort; +import static org.assertj.core.api.Assertions.assertThat; + +import com.helpmeCookies.global.config.QueryDSLConfig; +import com.helpmeCookies.product.repository.ProductRepository; +import com.helpmeCookies.product.util.ProductSort; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; + +@DataJpaTest +@ExtendWith(OutputCaptureExtension.class) +@Import(QueryDSLConfig.class) +public class SearchRepositoryTest { + + @Autowired + private ProductRepository productRepository; + + + @Test + @DisplayName("상품 검색 쿼리 확인") + void 상품_검색(CapturedOutput out) { + // given + var sort = convertProductSort(ProductSort.LATEST); + var pageRequest = PageRequest.of(0, 10, sort); + + // when + try{ + productRepository.findByNameWithIdx("product", pageRequest); + } catch (Exception ignored) { + } + + // then + assertThat(out.getOut()) + .contains("SELECT") + .contains("p.id,") + .contains("p.name,") + .contains("a.nickname AS artist,") + .contains("p.price") + .contains("FROM product p") + .contains("JOIN artist_info a ON p.artist_info_id = a.id") + .contains("WHERE MATCH(p.name) AGAINST (? IN BOOLEAN MODE)") + .contains("order by p.created_date desc"); + //.contains("limit ?") 테스트 dbh2 db에서는 limit이 없어서 제외 + } +} diff --git a/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java b/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java new file mode 100644 index 0000000..65c1edb --- /dev/null +++ b/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java @@ -0,0 +1,63 @@ +package com.helpmeCookies.product.search; + +import static com.helpmeCookies.product.util.SortUtil.convertProductSort; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import com.helpmeCookies.product.repository.ProductRepository; +import com.helpmeCookies.product.repository.dto.ProductSearch; +import com.helpmeCookies.product.service.ProductService; +import com.helpmeCookies.product.util.ProductSort; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +@TestComponent +@ExtendWith(MockitoExtension.class) +public class SearchServiceTest { + + @Mock + private ProductRepository productRepository; + + @InjectMocks + private ProductService productService; + + @Test + @DisplayName("상품 검색 서비스") + void 상품_검색() { + // given + var sort = convertProductSort(ProductSort.LATEST); + var pageRequest = PageRequest.of(0, 10, sort); + var productSearch = mock(ProductSearch.class); + given(productSearch.getId()).willReturn(1L); + given(productSearch.getName()).willReturn("product1"); + given(productSearch.getArtist()).willReturn("artist"); + given(productSearch.getPrice()).willReturn(10000L); + + var productPage = new PageImpl<>(List.of(productSearch)); + given(productRepository.findByNameWithIdx("roduct", pageRequest)) + .willReturn(productPage); + + // when + var result = productService.getProductsByPage("roduct", pageRequest); + + // then + assertAll( + () -> assertThat(result.hasNext()).isFalse(), + () -> assertThat(result.products().size()).isEqualTo(1L), + () -> assertThat(result.products().get(0).name()).isEqualTo("product1"), + () -> assertThat(result.products().get(0).artist()).isEqualTo("artist"), + () -> assertThat(result.products().get(0).price()).isEqualTo(10000L) + ); + } + +} From daaf61eb2bd9c2b848a8fdcd0e0bde19b432f05c Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 12:03:32 +0900 Subject: [PATCH 19/28] =?UTF-8?q?#27=20feat(product):=20Product=20thumbnai?= =?UTF-8?q?lUrl=20=EC=82=BD=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/helpmeCookies/product/entity/Product.java | 6 ++++++ .../helpmeCookies/product/service/ProductImageService.java | 6 ++++++ .../product/service/ProductImageServiceTest.java | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/helpmeCookies/product/entity/Product.java b/src/main/java/com/helpmeCookies/product/entity/Product.java index 584b32a..7f07fa3 100644 --- a/src/main/java/com/helpmeCookies/product/entity/Product.java +++ b/src/main/java/com/helpmeCookies/product/entity/Product.java @@ -44,6 +44,8 @@ public class Product extends BaseTimeEntity { @Column(nullable = false) private String preferredLocation; + private String thumbnailUrl; + @ElementCollection(targetClass = HashTag.class) @CollectionTable(name = "product_hashtags") @Enumerated(EnumType.STRING) @@ -113,4 +115,8 @@ public void update(String name, Category category, String size, Long price, Stri this.hashTags = hashTags; this.artistInfo = artistInfo; } + + public void updateThumbnail(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } } diff --git a/src/main/java/com/helpmeCookies/product/service/ProductImageService.java b/src/main/java/com/helpmeCookies/product/service/ProductImageService.java index 25215c5..0e34e78 100644 --- a/src/main/java/com/helpmeCookies/product/service/ProductImageService.java +++ b/src/main/java/com/helpmeCookies/product/service/ProductImageService.java @@ -3,6 +3,7 @@ import com.helpmeCookies.global.utils.AwsS3FileUtils; import com.helpmeCookies.product.dto.FileUploadResponse; import com.helpmeCookies.product.repository.ProductImageRepository; +import com.helpmeCookies.product.repository.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,12 +17,17 @@ public class ProductImageService { private final AwsS3FileUtils awsS3FileUtils; private final ProductImageRepository productImageRepository; + private final ProductRepository productRepository; @Transactional public List uploadMultiFiles(Long productId, List files) throws IOException { List uploadResponses = awsS3FileUtils.uploadMultiImages(files); uploadResponses.forEach(response -> productImageRepository.save(response.toEntity(productId))); + + var product = productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 id입니다")); + product.updateThumbnail(uploadResponses.getFirst().photoUrl()); + return uploadResponses; } diff --git a/src/test/java/com/helpmeCookies/product/service/ProductImageServiceTest.java b/src/test/java/com/helpmeCookies/product/service/ProductImageServiceTest.java index b0b3653..23cc74d 100644 --- a/src/test/java/com/helpmeCookies/product/service/ProductImageServiceTest.java +++ b/src/test/java/com/helpmeCookies/product/service/ProductImageServiceTest.java @@ -41,7 +41,7 @@ class ProductImageServiceTest { @BeforeEach void setUp() { - productImageService = new ProductImageService(awsS3FileUtils, productImageRepository); + productImageService = new ProductImageService(awsS3FileUtils, productImageRepository, productRepository); } @AfterEach From 58dad0c9e991cd015b978106b39552bd9f219414 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 12:07:50 +0900 Subject: [PATCH 20/28] =?UTF-8?q?#27=20feat(product):=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EC=8B=9C=20Product=20thumbnailUrl=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/helpmeCookies/product/dto/ProductPage.java | 6 ++++-- .../product/repository/ProductRepository.java | 7 +------ .../product/repository/dto/ProductSearch.java | 1 + 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/helpmeCookies/product/dto/ProductPage.java b/src/main/java/com/helpmeCookies/product/dto/ProductPage.java index 1648c19..9778e2f 100644 --- a/src/main/java/com/helpmeCookies/product/dto/ProductPage.java +++ b/src/main/java/com/helpmeCookies/product/dto/ProductPage.java @@ -10,14 +10,16 @@ public record Info( Long id, String name, String artist, - Long price + Long price, + String thumbnailUrl ) { public static Info from(ProductSearch productSearch) { return new Info( productSearch.getId(), productSearch.getName(), productSearch.getArtist(), - productSearch.getPrice() + productSearch.getPrice(), + productSearch.getThumbnailUrl() ); } diff --git a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java index 512722d..4b23b79 100644 --- a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java +++ b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java @@ -12,12 +12,7 @@ @Repository public interface ProductRepository extends JpaRepository { - @Query("SELECT p.id AS id, p.name AS name, a.nickname AS artist, p.price AS price " + - "FROM Product p JOIN p.artistInfo a " + - "WHERE p.name LIKE %:query%") // Index 미사용 - Page findByName(@Param("query") String query, Pageable pageable); - - @Query(value = "SELECT p.id, p.name, a.nickname AS artist, p.price " + + @Query(value = "SELECT p.id, p.name, p.thumbnail_url, a.nickname AS artist, p.price " + "FROM product p JOIN artist_info a ON p.artist_info_id = a.id " + "WHERE MATCH(p.name) AGAINST (:query IN BOOLEAN MODE)", countQuery = "SELECT COUNT(*) " + diff --git a/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java index 3c5fcee..c997acd 100644 --- a/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java +++ b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java @@ -6,4 +6,5 @@ public interface ProductSearch { String getName(); String getArtist(); Long getPrice(); + String getThumbnailUrl(); } From 2be345745c1f41308d0f8d271bb3ec6e3293328f Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 12:23:19 +0900 Subject: [PATCH 21/28] =?UTF-8?q?#27=20test(product):=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20thumbnailUrl=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helpmeCookies/product/repository/dto/ProductSearch.java | 2 +- .../helpmeCookies/product/search/SearchControllerTest.java | 6 ++++-- .../helpmeCookies/product/search/SearchRepositoryTest.java | 1 + .../com/helpmeCookies/product/search/SearchServiceTest.java | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java index c997acd..60ca2e0 100644 --- a/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java +++ b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java @@ -4,7 +4,7 @@ public interface ProductSearch { Long getId(); String getName(); + String getThumbnailUrl(); String getArtist(); Long getPrice(); - String getThumbnailUrl(); } diff --git a/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java b/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java index be84884..4566fe1 100644 --- a/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java +++ b/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java @@ -56,12 +56,13 @@ public class SearchControllerTest { given(productSearch.getName()).willReturn("product1"); given(productSearch.getArtist()).willReturn("artist"); given(productSearch.getPrice()).willReturn(10000L); + given(productSearch.getThumbnailUrl()).willReturn("thumbnailUrl"); var paging = ProductPage.Paging.from(new PageImpl<>(List.of(productSearch))); given(productService.getProductsByPage(eq(query), any(Pageable.class))) .willReturn(paging); // when & then - mvc.perform(get("/test/v1/products") + mvc.perform(get("/v1/products") .param("query", query) .param("size", String.valueOf(size)) .param("page", String.valueOf(page)) @@ -70,7 +71,8 @@ public class SearchControllerTest { .andExpect(jsonPath("$.hasNext").value(false)) .andExpect(jsonPath("$.products[0].name").value("product1")) .andExpect(jsonPath("$.products[0].artist").value("artist")) - .andExpect(jsonPath("$.products[0].price").value(10000L)); + .andExpect(jsonPath("$.products[0].price").value(10000L)) + .andExpect(jsonPath("$.products[0].thumbnailUrl").value("thumbnailUrl")); } } diff --git a/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java b/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java index b5f2ad0..e914d94 100644 --- a/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java +++ b/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java @@ -43,6 +43,7 @@ public class SearchRepositoryTest { .contains("SELECT") .contains("p.id,") .contains("p.name,") + .contains("p.thumbnail_url,") .contains("a.nickname AS artist,") .contains("p.price") .contains("FROM product p") diff --git a/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java b/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java index 65c1edb..a13d83d 100644 --- a/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java +++ b/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java @@ -42,6 +42,7 @@ public class SearchServiceTest { given(productSearch.getName()).willReturn("product1"); given(productSearch.getArtist()).willReturn("artist"); given(productSearch.getPrice()).willReturn(10000L); + given(productSearch.getThumbnailUrl()).willReturn("thumbnailUrl"); var productPage = new PageImpl<>(List.of(productSearch)); given(productRepository.findByNameWithIdx("roduct", pageRequest)) @@ -56,7 +57,8 @@ public class SearchServiceTest { () -> assertThat(result.products().size()).isEqualTo(1L), () -> assertThat(result.products().get(0).name()).isEqualTo("product1"), () -> assertThat(result.products().get(0).artist()).isEqualTo("artist"), - () -> assertThat(result.products().get(0).price()).isEqualTo(10000L) + () -> assertThat(result.products().get(0).price()).isEqualTo(10000L), + () -> assertThat(result.products().get(0).thumbnailUrl()).isEqualTo("thumbnailUrl") ); } From b9ac98aca46a3f31a42adcd7c53a048184f18b72 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:21:16 +0900 Subject: [PATCH 22/28] =?UTF-8?q?#27=20feat(user.repository):=20=EC=9E=91?= =?UTF-8?q?=EA=B0=80=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repository/ArtistInfoRepository.java | 18 +++++++++++++++--- .../user/repository/dto/ArtistSearch.java | 9 +++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/helpmeCookies/user/repository/dto/ArtistSearch.java diff --git a/src/main/java/com/helpmeCookies/user/repository/ArtistInfoRepository.java b/src/main/java/com/helpmeCookies/user/repository/ArtistInfoRepository.java index 49847d2..86da115 100644 --- a/src/main/java/com/helpmeCookies/user/repository/ArtistInfoRepository.java +++ b/src/main/java/com/helpmeCookies/user/repository/ArtistInfoRepository.java @@ -1,12 +1,24 @@ package com.helpmeCookies.user.repository; +import com.helpmeCookies.user.entity.ArtistInfo; +import com.helpmeCookies.user.repository.dto.ArtistSearch; import java.util.Optional; - +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; - -import com.helpmeCookies.user.entity.ArtistInfo; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ArtistInfoRepository extends JpaRepository { Optional findByUserId(Long userId); boolean existsByUserId(Long userId); + + @Query(value = "SELECT a.id, a.nickname, a.artist_image_url, a.total_followers, a.total_likes " + + "FROM artist_info a " + + "WHERE MATCH(a.nickname) AGAINST (:query IN BOOLEAN MODE)", + countQuery = "SELECT COUNT(*) " + + "FROM artist_info a " + + "WHERE MATCH(a.nickname) AGAINST (:query IN BOOLEAN MODE)", + nativeQuery = true) // Index 사용 + Page findByNicknameWithIdx(@Param("query") String query, Pageable pageable); } diff --git a/src/main/java/com/helpmeCookies/user/repository/dto/ArtistSearch.java b/src/main/java/com/helpmeCookies/user/repository/dto/ArtistSearch.java new file mode 100644 index 0000000..777ed6a --- /dev/null +++ b/src/main/java/com/helpmeCookies/user/repository/dto/ArtistSearch.java @@ -0,0 +1,9 @@ +package com.helpmeCookies.user.repository.dto; + +public interface ArtistSearch { + Long getId(); + String getNickname(); + String getArtistImageUrl(); + Long getTotalFollowers(); + Long getTotalLikes(); +} From 135135b01d3c11063b2380725d10cf05c36e8f6f Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:22:29 +0900 Subject: [PATCH 23/28] =?UTF-8?q?#27=20feat(user.service):=20=EC=9E=91?= =?UTF-8?q?=EA=B0=80=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helpmeCookies/user/service/ArtistService.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/helpmeCookies/user/service/ArtistService.java b/src/main/java/com/helpmeCookies/user/service/ArtistService.java index 3f63d32..c36475e 100644 --- a/src/main/java/com/helpmeCookies/user/service/ArtistService.java +++ b/src/main/java/com/helpmeCookies/user/service/ArtistService.java @@ -1,10 +1,8 @@ package com.helpmeCookies.user.service; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import com.helpmeCookies.global.exception.user.ResourceNotFoundException; import com.helpmeCookies.user.dto.ArtistInfoDto; +import com.helpmeCookies.user.dto.ArtistInfoPage; import com.helpmeCookies.user.dto.BusinessArtistDto; import com.helpmeCookies.user.dto.StudentArtistDto; import com.helpmeCookies.user.dto.request.BusinessArtistReq; @@ -20,8 +18,10 @@ import com.helpmeCookies.user.repository.StudentArtistRepository; import com.helpmeCookies.user.repository.UserRepository; import com.sun.jdi.request.DuplicateRequestException; - import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -115,4 +115,10 @@ public ArtistDetailsRes getArtistDetails(Long userId) { throw new ResourceNotFoundException("존재하지 않는 아티스트입니다."); } } + + @Transactional(readOnly = true) + public ArtistInfoPage.Paging getArtistsByPage(String query, Pageable pageable) { + var artistInfoPage = artistInfoRepository.findByNicknameWithIdx(query, pageable); + return ArtistInfoPage.Paging.from(artistInfoPage); + } } From 3e94711e67cf550b469f38f4eeebe5ee262877a0 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:22:35 +0900 Subject: [PATCH 24/28] =?UTF-8?q?#27=20feat(user.controller):=20=EC=9E=91?= =?UTF-8?q?=EA=B0=80=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/ArtistController.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/helpmeCookies/user/controller/ArtistController.java b/src/main/java/com/helpmeCookies/user/controller/ArtistController.java index 32a71f7..6e94168 100644 --- a/src/main/java/com/helpmeCookies/user/controller/ArtistController.java +++ b/src/main/java/com/helpmeCookies/user/controller/ArtistController.java @@ -1,24 +1,24 @@ package com.helpmeCookies.user.controller; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -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 org.springframework.web.bind.annotation.RestController; - -import com.helpmeCookies.global.jwt.JwtProvider; import com.helpmeCookies.global.jwt.JwtUser; import com.helpmeCookies.user.controller.apiDocs.ArtistApiDocs; +import com.helpmeCookies.user.dto.ArtistInfoPage; import com.helpmeCookies.user.dto.request.BusinessArtistReq; import com.helpmeCookies.user.dto.request.StudentArtistReq; import com.helpmeCookies.user.dto.response.ArtistDetailsRes; import com.helpmeCookies.user.service.ArtistService; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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 org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @@ -62,4 +62,14 @@ public ArtistDetailsRes getArtist( ) { return artistService.getArtistDetails(jwtUser.getId()); } + + @GetMapping("/v1/artists") + public ResponseEntity getArtistsByPage( + @RequestParam("query") String query, + @RequestParam(name = "size", required = false, defaultValue = "20") int size, + @RequestParam("page") int page + ) { + var pageable = PageRequest.of(page, size); + return ResponseEntity.ok(artistService.getArtistsByPage(query, pageable)); + } } From aff8acfc266f2811dc13e7e871f261b36687fa04 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:22:50 +0900 Subject: [PATCH 25/28] =?UTF-8?q?#27=20feat(user.dto):=20=EC=9E=91?= =?UTF-8?q?=EA=B0=80=20=EA=B2=80=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=95=20?= =?UTF-8?q?DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/ArtistInfoPage.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/com/helpmeCookies/user/dto/ArtistInfoPage.java diff --git a/src/main/java/com/helpmeCookies/user/dto/ArtistInfoPage.java b/src/main/java/com/helpmeCookies/user/dto/ArtistInfoPage.java new file mode 100644 index 0000000..725fb2e --- /dev/null +++ b/src/main/java/com/helpmeCookies/user/dto/ArtistInfoPage.java @@ -0,0 +1,46 @@ +package com.helpmeCookies.user.dto; + +import com.helpmeCookies.user.repository.dto.ArtistSearch; +import java.util.List; +import org.springframework.data.domain.Page; + +public class ArtistInfoPage { + + public record Info( + Long id, + String nickname, + String artistImageUrl, + Long totalFollowers, + Long totalLikes + ) { + + private static Info from(ArtistSearch artistSearch) { + return new Info( + artistSearch.getId(), + artistSearch.getNickname(), + artistSearch.getArtistImageUrl(), + artistSearch.getTotalFollowers(), + artistSearch.getTotalLikes() + ); + } + + public static List of(List content) { + return content.stream() + .map(Info::from) + .toList(); + } + } + + public record Paging ( + boolean hasNext, + List artists + ) { + public static Paging from(Page artistPage) { + return new Paging( + artistPage.hasNext(), + Info.of(artistPage.getContent()) + ); + } + } + +} From 285c4ce4130b4185681e3a15f36203c8edc8a1a3 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:23:07 +0900 Subject: [PATCH 26/28] =?UTF-8?q?#27=20docs(user):=20=EC=9E=91=EA=B0=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=EB=AC=B8=EC=84=9C=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/apiDocs/ArtistApiDocs.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/helpmeCookies/user/controller/apiDocs/ArtistApiDocs.java b/src/main/java/com/helpmeCookies/user/controller/apiDocs/ArtistApiDocs.java index 3f77c43..254c5b7 100644 --- a/src/main/java/com/helpmeCookies/user/controller/apiDocs/ArtistApiDocs.java +++ b/src/main/java/com/helpmeCookies/user/controller/apiDocs/ArtistApiDocs.java @@ -1,19 +1,19 @@ package com.helpmeCookies.user.controller.apiDocs; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -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 com.helpmeCookies.global.jwt.JwtUser; +import com.helpmeCookies.user.dto.ArtistInfoPage.Paging; import com.helpmeCookies.user.dto.request.BusinessArtistReq; import com.helpmeCookies.user.dto.request.StudentArtistReq; import com.helpmeCookies.user.dto.response.ArtistDetailsRes; - import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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; @Tag(name = "작가 관련 기능", description = "작가 관련 API") public interface ArtistApiDocs { @@ -44,4 +44,11 @@ ArtistDetailsRes getArtist( ArtistDetailsRes getArtist( @AuthenticationPrincipal JwtUser jwtUser ); + + @Operation(summary = "작가 검색") + ResponseEntity getArtistsByPage( + String query, + @Parameter(description = "default value 20") int size, + int page + ); } From 425881a950b1eda7e25e86aa2f70ac6ee5ba242c Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:37:01 +0900 Subject: [PATCH 27/28] =?UTF-8?q?#27=20test(user):=20=EC=9E=91=EA=B0=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/SearchControllerTest.java | 42 +++++++++++++++- .../search/SearchRepositoryTest.java | 31 +++++++++++- .../search/SearchServiceTest.java | 49 +++++++++++++++++-- 3 files changed, 114 insertions(+), 8 deletions(-) rename src/test/java/com/helpmeCookies/{product => }/search/SearchControllerTest.java (64%) rename src/test/java/com/helpmeCookies/{product => }/search/SearchRepositoryTest.java (68%) rename src/test/java/com/helpmeCookies/{product => }/search/SearchServiceTest.java (51%) diff --git a/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java b/src/test/java/com/helpmeCookies/search/SearchControllerTest.java similarity index 64% rename from src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java rename to src/test/java/com/helpmeCookies/search/SearchControllerTest.java index 4566fe1..f212587 100644 --- a/src/test/java/com/helpmeCookies/product/search/SearchControllerTest.java +++ b/src/test/java/com/helpmeCookies/search/SearchControllerTest.java @@ -1,4 +1,4 @@ -package com.helpmeCookies.product.search; +package com.helpmeCookies.search; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -15,6 +15,10 @@ import com.helpmeCookies.product.service.ProductImageService; import com.helpmeCookies.product.service.ProductService; import com.helpmeCookies.product.util.ProductSort; +import com.helpmeCookies.user.controller.ArtistController; +import com.helpmeCookies.user.dto.ArtistInfoPage; +import com.helpmeCookies.user.repository.dto.ArtistSearch; +import com.helpmeCookies.user.service.ArtistService; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -27,7 +31,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(ProductController.class) +@WebMvcTest({ProductController.class, ArtistController.class}) @AutoConfigureMockMvc(addFilters = false) @Import(JwtProvider.class) public class SearchControllerTest { @@ -41,6 +45,9 @@ public class SearchControllerTest { @MockBean private ProductImageService productImageService; + @MockBean + private ArtistService artistService; + @Test @DisplayName("상품 검색 컨트롤러") @@ -75,4 +82,35 @@ public class SearchControllerTest { .andExpect(jsonPath("$.products[0].thumbnailUrl").value("thumbnailUrl")); } + @Test + @DisplayName("작가 검색 컨트롤러") + void 작가_검색() throws Exception { + // given + String query = "nickname"; + int size = 10; + int page = 0; + + var artistSearch = mock(ArtistSearch.class); + given(artistSearch.getId()).willReturn(1L); + given(artistSearch.getNickname()).willReturn("nickname"); + given(artistSearch.getArtistImageUrl()).willReturn("artistImageUrl"); + given(artistSearch.getTotalFollowers()).willReturn(10000L); + given(artistSearch.getTotalLikes()).willReturn(10000L); + var paging = ArtistInfoPage.Paging.from(new PageImpl<>(List.of(artistSearch))); + given(artistService.getArtistsByPage(eq(query), any(Pageable.class))) + .willReturn(paging); + + // when & then + mvc.perform(get("/v1/artists") + .param("query", query) + .param("size", String.valueOf(size)) + .param("page", String.valueOf(page))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.hasNext").value(false)) + .andExpect(jsonPath("$.artists[0].nickname").value("nickname")) + .andExpect(jsonPath("$.artists[0].artistImageUrl").value("artistImageUrl")) + .andExpect(jsonPath("$.artists[0].totalFollowers").value(10000L)) + .andExpect(jsonPath("$.artists[0].totalLikes").value(10000L)); + } + } diff --git a/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java b/src/test/java/com/helpmeCookies/search/SearchRepositoryTest.java similarity index 68% rename from src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java rename to src/test/java/com/helpmeCookies/search/SearchRepositoryTest.java index e914d94..9b00725 100644 --- a/src/test/java/com/helpmeCookies/product/search/SearchRepositoryTest.java +++ b/src/test/java/com/helpmeCookies/search/SearchRepositoryTest.java @@ -1,4 +1,4 @@ -package com.helpmeCookies.product.search; +package com.helpmeCookies.search; import static com.helpmeCookies.product.util.SortUtil.convertProductSort; import static org.assertj.core.api.Assertions.assertThat; @@ -6,6 +6,7 @@ import com.helpmeCookies.global.config.QueryDSLConfig; import com.helpmeCookies.product.repository.ProductRepository; import com.helpmeCookies.product.util.ProductSort; +import com.helpmeCookies.user.repository.ArtistInfoRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +25,9 @@ public class SearchRepositoryTest { @Autowired private ProductRepository productRepository; + @Autowired + private ArtistInfoRepository artistInfoRepository; + @Test @DisplayName("상품 검색 쿼리 확인") @@ -52,4 +56,29 @@ public class SearchRepositoryTest { .contains("order by p.created_date desc"); //.contains("limit ?") 테스트 dbh2 db에서는 limit이 없어서 제외 } + + @Test + @DisplayName("작가 검색 쿼리 확인") + void 작가_검색(CapturedOutput out) { + // given + var pageRequest = PageRequest.of(0, 10); + + // when + try{ + artistInfoRepository.findByNicknameWithIdx("nickname", pageRequest); + } catch (Exception ignored) { + } + + // then + assertThat(out.getOut()) + .contains("SELECT") + .contains("a.id,") + .contains("a.nickname,") + .contains("a.artist_image_url,") + .contains("a.total_followers,") + .contains("a.total_likes") + .contains("FROM artist_info a") + .contains("WHERE MATCH(a.nickname) AGAINST (? IN BOOLEAN MODE)"); + } + } diff --git a/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java b/src/test/java/com/helpmeCookies/search/SearchServiceTest.java similarity index 51% rename from src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java rename to src/test/java/com/helpmeCookies/search/SearchServiceTest.java index a13d83d..4a9b738 100644 --- a/src/test/java/com/helpmeCookies/product/search/SearchServiceTest.java +++ b/src/test/java/com/helpmeCookies/search/SearchServiceTest.java @@ -1,4 +1,4 @@ -package com.helpmeCookies.product.search; +package com.helpmeCookies.search; import static com.helpmeCookies.product.util.SortUtil.convertProductSort; import static org.assertj.core.api.Assertions.assertThat; @@ -10,6 +10,9 @@ import com.helpmeCookies.product.repository.dto.ProductSearch; import com.helpmeCookies.product.service.ProductService; import com.helpmeCookies.product.util.ProductSort; +import com.helpmeCookies.user.repository.ArtistInfoRepository; +import com.helpmeCookies.user.repository.dto.ArtistSearch; +import com.helpmeCookies.user.service.ArtistService; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -31,6 +34,12 @@ public class SearchServiceTest { @InjectMocks private ProductService productService; + @Mock + private ArtistInfoRepository artistInfoRepository; + + @InjectMocks + private ArtistService artistService; + @Test @DisplayName("상품 검색 서비스") void 상품_검색() { @@ -55,10 +64,40 @@ public class SearchServiceTest { assertAll( () -> assertThat(result.hasNext()).isFalse(), () -> assertThat(result.products().size()).isEqualTo(1L), - () -> assertThat(result.products().get(0).name()).isEqualTo("product1"), - () -> assertThat(result.products().get(0).artist()).isEqualTo("artist"), - () -> assertThat(result.products().get(0).price()).isEqualTo(10000L), - () -> assertThat(result.products().get(0).thumbnailUrl()).isEqualTo("thumbnailUrl") + () -> assertThat(result.products().getFirst().name()).isEqualTo("product1"), + () -> assertThat(result.products().getFirst().artist()).isEqualTo("artist"), + () -> assertThat(result.products().getFirst().price()).isEqualTo(10000L), + () -> assertThat(result.products().getFirst().thumbnailUrl()).isEqualTo("thumbnailUrl") + ); + } + + @Test + @DisplayName("작가 검색 서비스") + void 작가_검색() { + // given + var pageRequest = PageRequest.of(0, 10); + var artistSearch = mock(ArtistSearch.class); + given(artistSearch.getId()).willReturn(1L); + given(artistSearch.getNickname()).willReturn("nickname"); + given(artistSearch.getArtistImageUrl()).willReturn("artistImageUrl"); + given(artistSearch.getTotalFollowers()).willReturn(10000L); + given(artistSearch.getTotalLikes()).willReturn(10000L); + + var artistPage = new PageImpl<>(List.of(artistSearch)); + given(artistInfoRepository.findByNicknameWithIdx("nickname", pageRequest)) + .willReturn(artistPage); + + // when + var result = artistService.getArtistsByPage("nickname", pageRequest); + + // then + assertAll( + () -> assertThat(result.hasNext()).isFalse(), + () -> assertThat(result.artists().size()).isEqualTo(1L), + () -> assertThat(result.artists().getFirst().nickname()).isEqualTo("nickname"), + () -> assertThat(result.artists().getFirst().artistImageUrl()).isEqualTo("artistImageUrl"), + () -> assertThat(result.artists().getFirst().totalFollowers()).isEqualTo(10000L), + () -> assertThat(result.artists().getFirst().totalLikes()).isEqualTo(10000L) ); } From 8478defed061f516220aada72d2840893ff8a7f5 Mon Sep 17 00:00:00 2001 From: sim-mer Date: Tue, 29 Oct 2024 13:37:32 +0900 Subject: [PATCH 28/28] =?UTF-8?q?#27=20test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repository/querydsl/UserCustomRepositoryImpl.java | 4 ++-- src/test/java/com/helpmeCookies/e2e/ArtistE2Etest.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/helpmeCookies/user/repository/querydsl/UserCustomRepositoryImpl.java b/src/main/java/com/helpmeCookies/user/repository/querydsl/UserCustomRepositoryImpl.java index bd31f40..2b176f0 100644 --- a/src/main/java/com/helpmeCookies/user/repository/querydsl/UserCustomRepositoryImpl.java +++ b/src/main/java/com/helpmeCookies/user/repository/querydsl/UserCustomRepositoryImpl.java @@ -33,8 +33,8 @@ public Page findFollowingUsers(Long userId, Pageable pageable) .select(Projections.constructor( UserFollowingDto.class, user.id, - user.userInfo.userImageUrl, - user.userInfo.nickname, + user.userImageUrl, + user.nickname, artistInfo.totalFollowers, artistInfo.totalLikes )) diff --git a/src/test/java/com/helpmeCookies/e2e/ArtistE2Etest.java b/src/test/java/com/helpmeCookies/e2e/ArtistE2Etest.java index 6d76c7b..3522fdc 100644 --- a/src/test/java/com/helpmeCookies/e2e/ArtistE2Etest.java +++ b/src/test/java/com/helpmeCookies/e2e/ArtistE2Etest.java @@ -45,7 +45,8 @@ public void testRegisterStudents_withValidToken() throws Exception { StudentArtistReq request = new StudentArtistReq( "student@example.com", "Example University", - "Computer Science" + "Computer Science", + "" ); String requestJson = objectMapper.writeValueAsString(request);