Skip to content
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

Merged
merged 45 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4521acd
feat: Review entity 작성
kariskan May 16, 2024
6531d84
feat: 상품에 대한 주문 주량, 평균 평점 column 추가
kariskan May 21, 2024
d9235d3
feat: 리뷰 추가 로직
kariskan May 21, 2024
d1146c9
feat: 리뷰 객체 관리하는 factory 클래스 작성
kariskan May 21, 2024
9cee426
fix: 상품 구매 확정 시 상품에 대한 구매 횟수 증가
kariskan May 21, 2024
7a4bc18
feat: 중복 리뷰 에러 코드 추가
kariskan May 21, 2024
417d558
feat: ReviewController 요청에 인터셉터 거치도록 수정
kariskan May 21, 2024
5f1e396
feat: ReviewController 작성
kariskan May 21, 2024
b9ba65f
feat: 상품 검색 기준 SortType 작성
kariskan May 21, 2024
720f044
feat: 상품 검색 Controller 작성
kariskan May 21, 2024
fcc83f1
feat: 검색 조건에 따른 쿼리 작성
kariskan May 21, 2024
55c8f38
feat: @ModelAttribute 검증기 작성
kariskan May 21, 2024
ab83678
fix: column definition 변경
kariskan May 21, 2024
ca6f424
fix: ddl-auto 옵션 변경
kariskan May 21, 2024
100aff7
test: ProductController test 추가
kariskan May 21, 2024
9dce566
test: ReviewController test 추가
kariskan May 21, 2024
eafc4f7
test: ProductReadService test 추가
kariskan May 21, 2024
ce558c8
test: ReviewService test 추가
kariskan May 21, 2024
58e11b7
style: reformat code
kariskan May 21, 2024
03f2f88
refactor: code smell 제거
kariskan May 21, 2024
ba453e6
test: entity 테스트 추가
kariskan May 21, 2024
dd0100d
refactor: @Min, @Max를 @Range로 변경
kariskan May 21, 2024
3bbbc72
refactor: 변수명 간소화
kariskan May 21, 2024
e110bca
refactor: 불필요한 import 제거
kariskan May 21, 2024
22ba3bf
refactor: magic number 제거
kariskan May 21, 2024
ae58853
fix: 해당 상품에 구매 이력이 존재하는 경우에만 리뷰 작성하도록 수정
kariskan May 21, 2024
4361cd9
test: ReviewServiceTest 추가
kariskan May 21, 2024
71fba2f
style: 주석 추가
kariskan May 21, 2024
415e5de
test: 상품 검색 테스트 수정
kariskan May 21, 2024
01d0876
test: @SpringBootTest 제거
kariskan May 21, 2024
edb548b
test: @SpringBootTest 제거
kariskan May 21, 2024
0a83f2b
refactor: 형식 일치, asc 제거
kariskan May 23, 2024
53064d6
refactor: requireNonNullElse 활용
kariskan May 23, 2024
b55169e
chore: ddl-auto 옵션 변경
kariskan May 23, 2024
83ffe38
fix: count 쿼리가 아닌 exists 쿼리로 변경
kariskan May 23, 2024
797fce9
refactor: factory class 의존성 삭제하고 static 메서드로 변경
kariskan May 23, 2024
ae37561
fix: http status code 변경
kariskan May 23, 2024
1d88ac1
fix: avgScore 고정 소수점 계산으로 변경
kariskan May 24, 2024
19ab651
feat: 동적 쿼리 MyBatis 도입
kariskan May 24, 2024
040c2b2
bug: 잘못된 쿼리 수정
kariskan May 24, 2024
5925e88
feat: ReviewFactory private 생성자 추가
kariskan May 24, 2024
6f3bb69
feat: OrderProduct Scheduler 추가
kariskan May 24, 2024
7116384
style: code reformat
kariskan May 24, 2024
1de95b3
chore: build.gradle 수정
kariskan May 24, 2024
2b9b59e
fix: error message 수정
kariskan May 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-lang3:3.13.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,18 @@ public void confirmOrder(Long orderId, Consumer consumer) {
order.updateOrderStatus(CONFIRM);
List<OrderProduct> orderProducts = orderProductReadService.findByOrderJoinFetchProductAndSeller(orderId);
addSellerBalance(orderProducts);
addOrderCount(orderProducts);

savePointLog(consumer, order, true);
}

/**
* 주문 시 product에 대한 구매 횟수를 증가함
*/
private void addOrderCount(List<OrderProduct> orderProducts) {
orderProducts.forEach(orderProduct -> orderProduct.getProduct().increaseOrderCount());
}

private void saveOrderInfo(PurchaseProductRequest request, Consumer consumer, Order order,
List<OrderProduct> orderProducts, long totalAmount) {
orderProductJdbcRepository.saveAllBatch(orderProducts);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private void validateRequest(UpdateDeliveryStatusRequest request, Delivery deliv
*/
private boolean isInvalidChangeStatus(DeliveryStatus future, DeliveryStatus current) {
return future.isPending()
|| (future.isDelivering() && !current.isPending())
|| (future.isDelivered() && !current.isDelivering());
|| (future.isDelivering() && !current.isPending())
|| (future.isDelivered() && !current.isDelivering());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package org.c4marathon.assignment.domain.order.repository;

import java.util.List;

import org.c4marathon.assignment.domain.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {

boolean existsByConsumerIdAndIdIn(Long consumerId, List<Long> ids);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
Expand All @@ -20,7 +21,12 @@
import lombok.NoArgsConstructor;

@Entity
@Table(name = "order_product_tbl")
@Table(
name = "order_product_tbl",
indexes = {
@Index(name = "created_at_idx", columnList = "created_at")
}
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class OrderProduct extends BaseEntity {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.c4marathon.assignment.domain.orderproduct.repository;

import java.time.LocalDateTime;
import java.util.List;

import org.c4marathon.assignment.domain.orderproduct.entity.OrderProduct;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -14,4 +16,16 @@ public interface OrderProductRepository extends JpaRepository<OrderProduct, Long

@Query("select op from OrderProduct op join fetch op.product join fetch op.product.seller where op.order.id = :id")
List<OrderProduct> findByOrderJoinFetchProductAndSeller(@Param("id") Long orderId);

@Query(value = "select order_id from order_product_tbl op where op.product_id = :product_id", nativeQuery = true)
List<Long> findOrderIdByProductId(@Param("product_id") Long id);

@Modifying
@Query(value = """
delete
from order_product_tbl op
where op.created_at <= :dateTime
limit :pageSize
""", nativeQuery = true)
int deleteOrderProductTable(@Param("dateTime") LocalDateTime dateTime, @Param("pageSize") int pageSize);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.c4marathon.assignment.domain.order.repository.OrderRepository;
import org.c4marathon.assignment.domain.orderproduct.entity.OrderProduct;
import org.c4marathon.assignment.domain.orderproduct.repository.OrderProductRepository;
import org.springframework.stereotype.Service;
Expand All @@ -14,6 +15,7 @@
public class OrderProductReadService {

private final OrderProductRepository orderProductRepository;
private final OrderRepository orderRepository;

/**
* Product와 조인한 OrderProduct list를 orderId로 조회
Expand All @@ -30,4 +32,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 orderRepository.existsByConsumerIdAndIdIn(consumerId, orderIds);
}
}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가능하면 @ModelAttribute 보다는 @RequestBody 를 사용하면 좋을 것 같아요.
참고: https://tecoble.techcourse.co.kr/post/2021-05-11-requestbody-modelattribute/

Copy link
Author

@kariskan kariskan May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이유가 왜일까요? 참고 문서에서는 @RequestBody는 http body가 java object로 변환되고, @ModelAttribute는 form or query param을 java object로 변환된다고 하는데 GetMapping에서는 body를 통한 데이터 전달을 지양하고, 보통 query string으로 요청을 하는 것으로 알고 있어서요.
차이가 있다면 RequestBody는 기본 생성자 + getter or setter이고 ModelAttribute는 생성자 or setter인 것 같습니다.
그런데 record로 정의하면 생성자, getter, 기타 등등 포함되기 때문에 어떤 형태로 오든 변환이 가능하지 않나요?

ProductSearchRequest.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.c4marathon.assignment.domain.product.dto.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Objects;
import org.c4marathon.assignment.global.constant.SortType;

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) {
    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 = (LocalDateTime)Objects.requireNonNullElse(createdAt, LocalDateTime.now());
        this.productId = (Long)Objects.requireNonNullElse(productId, Long.MIN_VALUE);
        this.amount = (Long)Objects.requireNonNullElse(amount, this.getDefaultAmount(sortType));
        this.orderCount = (Long)Objects.requireNonNullElse(orderCount, Long.MAX_VALUE);
        this.score = (Double)Objects.requireNonNullElse(score, Double.MAX_VALUE);
        this.pageSize = pageSize;
    }

    private Long getDefaultAmount(SortType sortType) {
        return sortType == SortType.PRICE_ASC ? Long.MIN_VALUE : Long.MAX_VALUE;
    }

    public static ProductSearchRequestBuilder builder() {
        return new ProductSearchRequestBuilder();
    }

    public @NotNull @Size(
    min = 2,
    message = "keyword length less than 2"
) String keyword() {
        return this.keyword;
    }

    public @NotNull SortType sortType() {
        return this.sortType;
    }

    public LocalDateTime createdAt() {
        return this.createdAt;
    }

    public Long productId() {
        return this.productId;
    }

    public Long amount() {
        return this.amount;
    }

    public Long orderCount() {
        return this.orderCount;
    }

    public Double score() {
        return this.score;
    }

    public int pageSize() {
        return this.pageSize;
    }

    public static class ProductSearchRequestBuilder {
        private String keyword;
        private SortType sortType;
        private LocalDateTime createdAt;
        private Long productId;
        private Long amount;
        private Long orderCount;
        private Double score;
        private int pageSize;

        ProductSearchRequestBuilder() {
        }

        public ProductSearchRequestBuilder keyword(final String keyword) {
            this.keyword = keyword;
            return this;
        }

        public ProductSearchRequestBuilder sortType(final SortType sortType) {
            this.sortType = sortType;
            return this;
        }

        public ProductSearchRequestBuilder createdAt(final LocalDateTime createdAt) {
            this.createdAt = createdAt;
            return this;
        }

        public ProductSearchRequestBuilder productId(final Long productId) {
            this.productId = productId;
            return this;
        }

        public ProductSearchRequestBuilder amount(final Long amount) {
            this.amount = amount;
            return this;
        }

        public ProductSearchRequestBuilder orderCount(final Long orderCount) {
            this.orderCount = orderCount;
            return this;
        }

        public ProductSearchRequestBuilder score(final Double score) {
            this.score = score;
            return this;
        }

        public ProductSearchRequestBuilder pageSize(final int pageSize) {
            this.pageSize = pageSize;
            return this;
        }

        public ProductSearchRequest build() {
            return new ProductSearchRequest(this.keyword, this.sortType, this.createdAt, this.productId, this.amount, this.orderCount, this.score, this.pageSize);
        }

        public String toString() {
            return "ProductSearchRequest.ProductSearchRequestBuilder(keyword=" + this.keyword + ", sortType=" + this.sortType + ", createdAt=" + this.createdAt + ", productId=" + this.productId + ", amount=" + this.amount + ", orderCount=" + this.orderCount + ", score=" + this.score + ", pageSize=" + this.pageSize + ")";
        }
    }
}

Copy link
Member

Choose a reason for hiding this comment

The 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,48 @@
package org.c4marathon.assignment.domain.product.dto.request;

import static java.util.Objects.*;

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 = requireNonNullElse(createdAt, LocalDateTime.now());
this.productId = requireNonNullElse(productId, Long.MIN_VALUE);
this.amount = requireNonNullElse(amount, getDefaultAmount(sortType));
this.orderCount = requireNonNullElse(orderCount, Long.MAX_VALUE);
this.score = requireNonNullElse(score, Double.MAX_VALUE);
this.pageSize = pageSize;
}

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,8 @@
package org.c4marathon.assignment.domain.product.dto.response;

import java.util.List;

public record ProductSearchResponse(
List<ProductSearchEntry> productSearchEntries
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.c4marathon.assignment.global.constant.ProductStatus.*;

import java.math.BigDecimal;

import org.c4marathon.assignment.domain.base.entity.BaseEntity;
import org.c4marathon.assignment.domain.seller.entity.Seller;
import org.c4marathon.assignment.global.constant.ProductStatus;
Expand All @@ -28,7 +30,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"),
@Index(name = "order_count_desc_product_id_idx", columnList = "order_count desc, product_id")
}
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down Expand Up @@ -64,6 +71,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 BigDecimal avgScore;

@NotNull
@Column(name = "order_count", columnDefinition = "BIGINT DEFAULT 0")
private Long orderCount;

@Builder
public Product(
String name,
Expand All @@ -78,6 +93,8 @@ public Product(
this.stock = stock;
this.seller = seller;
this.productStatus = IN_STOCK;
this.orderCount = 0L;
this.avgScore = BigDecimal.ZERO;
}

public void decreaseStock(Integer quantity) {
Expand All @@ -90,4 +107,12 @@ public void decreaseStock(Integer quantity) {
public boolean isSoldOut() {
return this.productStatus == OUT_OF_STOCK;
}

public void increaseOrderCount() {
this.orderCount++;
}

public void updateAvgScore(BigDecimal avgScore) {
this.avgScore = avgScore;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.c4marathon.assignment.domain.product.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.c4marathon.assignment.domain.product.dto.request.ProductSearchRequest;
import org.c4marathon.assignment.domain.product.dto.response.ProductSearchEntry;

@Mapper
public interface ProductMapper {
List<ProductSearchEntry> selectByCondition(ProductSearchRequest request);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.c4marathon.assignment.domain.product.service;

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.ProductMapper;
import org.c4marathon.assignment.domain.product.repository.ProductRepository;
import org.c4marathon.assignment.domain.seller.entity.Seller;
import org.c4marathon.assignment.global.error.ErrorCode;
Expand All @@ -14,6 +17,7 @@
public class ProductReadService {

private final ProductRepository productRepository;
private final ProductMapper productMapper;

/**
* Seller와 product.name으로 Product 존재 여부 확인
Expand All @@ -31,4 +35,17 @@ public Product findById(Long id) {
return productRepository.findByIdJoinFetch(id)
.orElseThrow(() -> ErrorCode.PRODUCT_NOT_FOUND.baseException("id: %d", id));
}

/**
* 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) {
return new ProductSearchResponse(productMapper.selectByCondition(request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.c4marathon.assignment.domain.review.controller;

import org.c4marathon.assignment.domain.consumer.entity.Consumer;
import org.c4marathon.assignment.domain.review.dto.request.ReviewCreateRequest;
import org.c4marathon.assignment.domain.review.service.ReviewService;
import org.c4marathon.assignment.global.auth.ConsumerThreadLocal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/reviews")
public class ReviewController {

private final ReviewService reviewService;

@PostMapping
public void createReview(@Valid @RequestBody ReviewCreateRequest request) {
Consumer consumer = ConsumerThreadLocal.get();
reviewService.createReview(consumer, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.c4marathon.assignment.domain.review.dto.request;

import org.hibernate.validator.constraints.Range;

import jakarta.validation.constraints.Size;

public record ReviewCreateRequest(
@Range(min = 1, max = 5, message = "invalid score range")
int score,
@Size(max = 100, message = "comment length more than 100")
String comment,
long productId
) {
}
Loading
Loading