-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/kariskan step3 #13
Changes from 31 commits
4521acd
6531d84
d9235d3
d1146c9
9cee426
7a4bc18
417d558
5f1e396
b9ba65f
720f044
fcc83f1
55c8f38
ab83678
ca6f424
100aff7
9dce566
eafc4f7
ce558c8
58e11b7
03f2f88
ba453e6
dd0100d
3bbbc72
e110bca
22ba3bf
ae58853
4361cd9
71fba2f
415e5de
01d0876
edb548b
0a83f2b
53064d6
b55169e
83ffe38
797fce9
ae37561
1d88ac1
19ab651
040c2b2
5925e88
6f3bb69
7116384
1de95b3
2b9b59e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,4 +30,13 @@ public List<OrderProduct> findByOrderJoinFetchProduct(Long orderId) { | |
public List<OrderProduct> findByOrderJoinFetchProductAndSeller(Long orderId) { | ||
return orderProductRepository.findByOrderJoinFetchProductAndSeller(orderId); | ||
} | ||
|
||
/** | ||
* consumer id와 product id로 구매 이력 조회 | ||
*/ | ||
@Transactional(readOnly = true) | ||
public boolean existsByConsumerIdAndProductId(Long consumerId, Long productId) { | ||
List<Long> orderIds = orderProductRepository.findOrderIdByProductId(productId); | ||
return orderProductRepository.countByIdsAndConsumerId(orderIds, consumerId) > 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그냥 exist query를 날릴 순 없을까요? select 1
from ~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그렇네요 굳이 셀 필요가 없네요. exists query로 수정했습니다. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package org.c4marathon.assignment.domain.product.controller; | ||
|
||
import org.c4marathon.assignment.domain.product.dto.request.ProductSearchRequest; | ||
import org.c4marathon.assignment.domain.product.dto.response.ProductSearchResponse; | ||
import org.c4marathon.assignment.domain.product.service.ProductReadService; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.ModelAttribute; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/products") | ||
public class ProductController { | ||
|
||
private final ProductReadService productReadService; | ||
|
||
@GetMapping | ||
public ProductSearchResponse searchProduct( | ||
@Valid @ModelAttribute ProductSearchRequest request | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 가능하면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이유가 왜일까요? 참고 문서에서는 ProductSearchRequest.class
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 Get이었네요 ㅋㅋ |
||
) { | ||
return productReadService.searchProduct(request); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package org.c4marathon.assignment.domain.product.dto.request; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
import org.c4marathon.assignment.global.constant.SortType; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.Builder; | ||
|
||
public record ProductSearchRequest( | ||
@NotNull @Size(min = 2, message = "keyword length less than 2") String keyword, | ||
@NotNull SortType sortType, | ||
LocalDateTime createdAt, | ||
Long productId, | ||
Long amount, | ||
Long orderCount, | ||
Double score, | ||
int pageSize | ||
) { | ||
|
||
@Builder | ||
public ProductSearchRequest( | ||
String keyword, | ||
SortType sortType, | ||
LocalDateTime createdAt, | ||
Long productId, | ||
Long amount, | ||
Long orderCount, | ||
Double score, | ||
int pageSize | ||
) { | ||
this.keyword = keyword; | ||
this.sortType = sortType; | ||
this.createdAt = setDefaultValue(createdAt, LocalDateTime.now()); | ||
this.productId = setDefaultValue(productId, Long.MIN_VALUE); | ||
this.amount = setDefaultValue(amount, getDefaultAmount(sortType)); | ||
this.orderCount = setDefaultValue(orderCount, Long.MAX_VALUE); | ||
this.score = setDefaultValue(score, Double.MAX_VALUE); | ||
this.pageSize = pageSize; | ||
} | ||
|
||
private <T> T setDefaultValue(T value, T defaultValue) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 메서드가 이런곳에서 쓰는 것이었군요.. |
||
return value != null ? value : defaultValue; | ||
} | ||
|
||
private Long getDefaultAmount(SortType sortType) { | ||
return sortType == SortType.PRICE_ASC ? Long.MIN_VALUE : Long.MAX_VALUE; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package org.c4marathon.assignment.domain.product.dto.response; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
public record ProductSearchEntry( | ||
long id, | ||
String name, | ||
String description, | ||
long amount, | ||
int stock, | ||
LocalDateTime createdAt, | ||
Long orderCount, | ||
Double avgScore | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.c4marathon.assignment.domain.product.dto.response; | ||
|
||
import java.util.List; | ||
|
||
import org.c4marathon.assignment.domain.product.entity.Product; | ||
|
||
public record ProductSearchResponse( | ||
List<ProductSearchEntry> productSearchEntries | ||
) { | ||
public static ProductSearchResponse of(List<Product> products) { | ||
return new ProductSearchResponse(products.stream() | ||
.map(product -> new ProductSearchEntry( | ||
product.getId(), | ||
product.getName(), | ||
product.getDescription(), | ||
product.getAmount(), | ||
product.getStock(), | ||
product.getCreatedAt(), | ||
product.getOrderCount(), | ||
product.getAvgScore())) | ||
.toList()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,12 @@ | |
@Table( | ||
name = "product_tbl", | ||
indexes = { | ||
@Index(name = "product_name_seller_id_idx", columnList = "name,seller_id") | ||
@Index(name = "product_name_seller_id_idx", columnList = "name,seller_id"), | ||
@Index(name = "amount_product_id_idx", columnList = "amount, product_id"), | ||
@Index(name = "amount_desc_product_id_idx", columnList = "amount desc, product_id"), | ||
@Index(name = "created_at_product_id_idx", columnList = "created_at desc, product_id"), | ||
@Index(name = "avg_score_desc_product_id_idx", columnList = "avg_score desc, product_id asc"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 형식 일치가 필요합니다. 여기에만 asc가 있네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 세세한 리뷰 감사합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기왕이면 적는게 좋습니다. |
||
@Index(name = "order_count_desc_product_id_idx", columnList = "order_count desc, product_id") | ||
} | ||
) | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
|
@@ -64,6 +69,14 @@ public class Product extends BaseEntity { | |
@Column(name = "status", columnDefinition = "VARCHAR(20)") | ||
private ProductStatus productStatus; | ||
|
||
@NotNull | ||
@Column(name = "avg_score", columnDefinition = "DECIMAL(5, 4) DEFAULT 0.0000") | ||
private Double avgScore; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 가능하면 해당 로직에 대해선 이후 성능 테스트를 꼭 진행해 주세요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리뷰 동시에 날렸을 때 같은 avgScore를 읽고 update 하면 동시성 문제가 발생할 수도 있겠네요. |
||
|
||
@NotNull | ||
@Column(name = "order_count", columnDefinition = "BIGINT DEFAULT 0") | ||
private Long orderCount; | ||
|
||
@Builder | ||
public Product( | ||
String name, | ||
|
@@ -78,6 +91,8 @@ public Product( | |
this.stock = stock; | ||
this.seller = seller; | ||
this.productStatus = IN_STOCK; | ||
this.orderCount = 0L; | ||
this.avgScore = (double)0; | ||
} | ||
|
||
public void decreaseStock(Integer quantity) { | ||
|
@@ -90,4 +105,12 @@ public void decreaseStock(Integer quantity) { | |
public boolean isSoldOut() { | ||
return this.productStatus == OUT_OF_STOCK; | ||
} | ||
|
||
public void increaseOrderCount() { | ||
this.orderCount++; | ||
} | ||
|
||
public void updateAvgScore(Double avgScore) { | ||
this.avgScore = avgScore; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
package org.c4marathon.assignment.domain.product.repository; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import org.c4marathon.assignment.domain.product.entity.Product; | ||
|
@@ -18,4 +20,88 @@ public interface ProductRepository extends JpaRepository<Product, Long> { | |
@Lock(LockModeType.PESSIMISTIC_WRITE) | ||
@Query("select p from Product p join fetch p.seller where p.id = :id") | ||
Optional<Product> findByIdJoinFetch(@Param("id") Long id); | ||
|
||
@Query(value = """ | ||
select count(*) | ||
from product_tbl p | ||
where p.product_id = :product_id | ||
""", nativeQuery = true) | ||
Long findReviewCount(@Param("product_id") Long id); | ||
|
||
@Query(value = """ | ||
select * | ||
from product_tbl p | ||
where name like :keyword | ||
and (p.created_at < :createdAt or (p.created_at = :createdAt and p.product_id > :id)) | ||
order by p.created_at desc, p.product_id | ||
limit :pageSize | ||
""", nativeQuery = true) | ||
List<Product> findByNewest( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5개 메소드가 특정 컬럼을 기준으로 정렬한 값을 리턴하는 것 같은데 이걸 하나의 메소드로 사용하는건 어떻게 생각하시나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 마 바 조 아 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MyBatis 적용했는데 이런 상황에선 굉장히 좋은것 같네요 굿굿 |
||
@Param("keyword") String keyword, | ||
@Param("createdAt") LocalDateTime createdAt, | ||
@Param("id") Long id, | ||
@Param("pageSize") int pageSize | ||
); | ||
|
||
@Query(value = """ | ||
select * | ||
from product_tbl p | ||
where name like :keyword | ||
and (p.amount > :amount or (p.amount = :amount and p.product_id > :id)) | ||
order by p.amount, p.product_id | ||
limit :pageSize | ||
""", nativeQuery = true) | ||
List<Product> findByPriceAsc( | ||
@Param("keyword") String keyword, | ||
@Param("amount") Long amount, | ||
@Param("id") Long id, | ||
@Param("pageSize") int pageSize | ||
); | ||
|
||
@Query(value = """ | ||
select * | ||
from product_tbl p | ||
where (p.amount < :amount or (p.amount = :amount and p.product_id > :id)) | ||
and name like :keyword | ||
order by p.amount desc, p.product_id | ||
limit :pageSize | ||
""", nativeQuery = true) | ||
List<Product> findByPriceDesc( | ||
@Param("keyword") String keyword, | ||
@Param("amount") Long amount, | ||
@Param("id") Long id, | ||
@Param("pageSize") int pageSize | ||
); | ||
|
||
@Query(value = """ | ||
select * | ||
from product_tbl | ||
where name like :keyword | ||
and order_count <= :order_count | ||
or (order_count = :order_count and product_id > :product_id) | ||
order by order_count desc, product_id | ||
limit :pageSize | ||
""", nativeQuery = true) | ||
List<Product> findByPopularity( | ||
@Param("keyword") String keyword, | ||
@Param("order_count") Long orderCount, | ||
@Param("product_id") Long id, | ||
@Param("pageSize") int pageSize | ||
); | ||
|
||
@Query(value = """ | ||
select * | ||
from product_tbl | ||
where name like :keyword | ||
and avg_score <= :avgScore | ||
or (avg_score = :avgScore and product_id > :product_id) | ||
order by avg_score desc, product_id | ||
limit :pageSize | ||
""", nativeQuery = true) | ||
List<Product> findByTopRated( | ||
@Param("keyword") String keyword, | ||
@Param("avgScore") Double avgScore, | ||
@Param("product_id") Long id, | ||
@Param("pageSize") int pageSize | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
package org.c4marathon.assignment.domain.product.service; | ||
|
||
import java.util.List; | ||
|
||
import org.c4marathon.assignment.domain.product.dto.request.ProductSearchRequest; | ||
import org.c4marathon.assignment.domain.product.dto.response.ProductSearchResponse; | ||
import org.c4marathon.assignment.domain.product.entity.Product; | ||
import org.c4marathon.assignment.domain.product.repository.ProductRepository; | ||
import org.c4marathon.assignment.domain.seller.entity.Seller; | ||
|
@@ -31,4 +35,37 @@ public Product findById(Long id) { | |
return productRepository.findByIdJoinFetch(id) | ||
.orElseThrow(() -> ErrorCode.PRODUCT_NOT_FOUND.baseException("id: %d", id)); | ||
} | ||
|
||
@Transactional(readOnly = true) | ||
public Long findReviewCount(Long productId) { | ||
return productRepository.findReviewCount(productId); | ||
} | ||
|
||
/** | ||
* sortType에 해당하는 조건으로 pagination | ||
* Newest: 최신순, created_at desc, product_id asc | ||
* PriceAsc: 가격 낮은 순, amount asc, product_id asc | ||
* PriceDesc: 가격 높은 순, amount desc, product_id asc | ||
* Popularity: 인기 순(주문 많은 순), order_count desc, product_id asc | ||
* TopRated: 평점 높은 순(review score), avg_score desc, product_id asc | ||
*/ | ||
@Transactional(readOnly = true) | ||
public ProductSearchResponse searchProduct(ProductSearchRequest request) { | ||
String keyword = toQueryKeyword(request.keyword()); | ||
Long productId = request.productId(); | ||
int pageSize = request.pageSize(); | ||
|
||
List<Product> products = switch (request.sortType()) { | ||
case NEWEST -> productRepository.findByNewest(keyword, request.createdAt(), productId, pageSize); | ||
case PRICE_ASC -> productRepository.findByPriceAsc(keyword, request.amount(), productId, pageSize); | ||
case PRICE_DESC -> productRepository.findByPriceDesc(keyword, request.amount(), productId, pageSize); | ||
case POPULARITY -> productRepository.findByPopularity(keyword, request.orderCount(), productId, pageSize); | ||
case TOP_RATED -> productRepository.findByTopRated(keyword, request.score(), productId, pageSize); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위의 쿼리를 동적 쿼리로 바꾸면 여기서도 switch case가 아닌 입력값만 전부 받아서 하나의 메소드만 호출하는 것도 좋을 것 같아요. |
||
}; | ||
return ProductSearchResponse.of(products); | ||
} | ||
|
||
private String toQueryKeyword(String keyword) { | ||
return "%" + keyword + "%"; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다 계산 하지 말고 조건에 만족하는 게 1개 있으면 true 반환하는 건 어떤가요!?
OrderProductReadService 의
보면 결국 있는 지 여부만 찾는 거 같아서요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exists로 수정했씁니다!