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() } 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; +} diff --git a/src/main/java/com/helpmeCookies/product/controller/ProductController.java b/src/main/java/com/helpmeCookies/product/controller/ProductController.java index 81afa0a..59e4985 100644 --- a/src/main/java/com/helpmeCookies/product/controller/ProductController.java +++ b/src/main/java/com/helpmeCookies/product/controller/ProductController.java @@ -1,13 +1,19 @@ package com.helpmeCookies.product.controller; import com.helpmeCookies.product.dto.ImageUpload; +import static com.helpmeCookies.product.util.SortUtil.convertProductSort; + +import com.helpmeCookies.product.controller.docs.ProductApiDocs; import com.helpmeCookies.product.dto.ProductImageResponse; +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 org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -17,7 +23,7 @@ @RestController @RequestMapping("/v1/products") @RequiredArgsConstructor -public class ProductController { +public class ProductController implements ProductApiDocs { private final ProductService productService; private final ProductImageService productImageService; @@ -63,4 +69,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/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 + ); + +} 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..9778e2f --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/dto/ProductPage.java @@ -0,0 +1,46 @@ +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, + String thumbnailUrl + ) { + public static Info from(ProductSearch productSearch) { + return new Info( + productSearch.getId(), + productSearch.getName(), + productSearch.getArtist(), + productSearch.getPrice(), + productSearch.getThumbnailUrl() + ); + } + + 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()) + ); + } + } + +} diff --git a/src/main/java/com/helpmeCookies/product/entity/Product.java b/src/main/java/com/helpmeCookies/product/entity/Product.java index d44b972..7f07fa3 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) @@ -42,12 +44,15 @@ public class Product { @Column(nullable = false) private String preferredLocation; + private String thumbnailUrl; + @ElementCollection(targetClass = HashTag.class) @CollectionTable(name = "product_hashtags") @Enumerated(EnumType.STRING) private List hashTags; @ManyToOne + @JoinColumn(name = "artist_info_id") private ArtistInfo artistInfo; public Product() {} @@ -110,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/repository/ProductRepository.java b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java index d8b05f3..4b23b79 100644 --- a/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java +++ b/src/main/java/com/helpmeCookies/product/repository/ProductRepository.java @@ -1,9 +1,23 @@ 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(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(*) " + + "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..60ca2e0 --- /dev/null +++ b/src/main/java/com/helpmeCookies/product/repository/dto/ProductSearch.java @@ -0,0 +1,10 @@ +package com.helpmeCookies.product.repository.dto; + + +public interface ProductSearch { + Long getId(); + String getName(); + String getThumbnailUrl(); + String getArtist(); + Long getPrice(); +} diff --git a/src/main/java/com/helpmeCookies/product/service/ProductImageService.java b/src/main/java/com/helpmeCookies/product/service/ProductImageService.java index 42948a1..f8de688 100644 --- a/src/main/java/com/helpmeCookies/product/service/ProductImageService.java +++ b/src/main/java/com/helpmeCookies/product/service/ProductImageService.java @@ -5,6 +5,7 @@ import com.helpmeCookies.product.entity.ProductImage; import com.helpmeCookies.product.repository.ProductImageRepository; import java.util.ArrayList; +import com.helpmeCookies.product.repository.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +18,7 @@ public class ProductImageService { private final AwsS3FileUtils awsS3FileUtils; private final ProductImageRepository productImageRepository; + private final ProductRepository productRepository; @Transactional public List uploadMultiFiles(List files) { diff --git a/src/main/java/com/helpmeCookies/product/service/ProductService.java b/src/main/java/com/helpmeCookies/product/service/ProductService.java index b288c53..999e247 100644 --- a/src/main/java/com/helpmeCookies/product/service/ProductService.java +++ b/src/main/java/com/helpmeCookies/product/service/ProductService.java @@ -7,7 +7,9 @@ import com.helpmeCookies.product.repository.ProductRepository; import com.helpmeCookies.user.entity.ArtistInfo; import com.helpmeCookies.user.repository.ArtistInfoRepository; +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; @@ -18,6 +20,12 @@ public class ProductService { private final ProductImageRepository productImageRepository; private final ArtistInfoRepository artistInfoRepository; + @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) { ArtistInfo artistInfo = artistInfoRepository.findById(productSaveRequest.artistInfoId()) .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 작가 정보입니다.")); 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()) + ); + } + } + +} 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..e85e068 --- /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("created_date")); + default -> Sort.unsorted(); + }; + } + +} 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)); + } } 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 + ); } 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()) + ); + } + } + +} 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(); +} 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/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); + } } 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); 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 diff --git a/src/test/java/com/helpmeCookies/search/SearchControllerTest.java b/src/test/java/com/helpmeCookies/search/SearchControllerTest.java new file mode 100644 index 0000000..f212587 --- /dev/null +++ b/src/test/java/com/helpmeCookies/search/SearchControllerTest.java @@ -0,0 +1,116 @@ +package com.helpmeCookies.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 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; +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, ArtistController.class}) +@AutoConfigureMockMvc(addFilters = false) +@Import(JwtProvider.class) +public class SearchControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private ProductService productService; + + @MockBean + private ProductImageService productImageService; + + @MockBean + private ArtistService artistService; + + + @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); + 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("/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)) + .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/search/SearchRepositoryTest.java b/src/test/java/com/helpmeCookies/search/SearchRepositoryTest.java new file mode 100644 index 0000000..9b00725 --- /dev/null +++ b/src/test/java/com/helpmeCookies/search/SearchRepositoryTest.java @@ -0,0 +1,84 @@ +package com.helpmeCookies.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 com.helpmeCookies.user.repository.ArtistInfoRepository; +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; + + @Autowired + private ArtistInfoRepository artistInfoRepository; + + + @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("p.thumbnail_url,") + .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이 없어서 제외 + } + + @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/search/SearchServiceTest.java b/src/test/java/com/helpmeCookies/search/SearchServiceTest.java new file mode 100644 index 0000000..4a9b738 --- /dev/null +++ b/src/test/java/com/helpmeCookies/search/SearchServiceTest.java @@ -0,0 +1,104 @@ +package com.helpmeCookies.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 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; +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; + + @Mock + private ArtistInfoRepository artistInfoRepository; + + @InjectMocks + private ArtistService artistService; + + @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); + given(productSearch.getThumbnailUrl()).willReturn("thumbnailUrl"); + + 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().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) + ); + } + +}