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

[#15] 마이페이지 쿠폰 조회 & 사용 가능한 쿠폰 조회 & 쿠폰 사용 #16

Merged
merged 29 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f1fe151
docs : README.md 수정
codesejin Jan 24, 2024
02691a4
docs : Update README.md
codesejin Jan 24, 2024
8d2ad96
자동 PR 리뷰를 위한 workflow 파일 추가 by f-lab
f-lab-bot Jan 24, 2024
c9d9a2f
chore : "JPA"에서 "Mybatis"로 변경
codesejin Jan 27, 2024
08d2332
Merge pull request from master
codesejin Jan 27, 2024
7121a2a
docs : Read Me ERD 등 추가
codesejin Jan 27, 2024
6c419e5
feat : 마이페이지 갖고 있는 쿠폰 조회 & 주문 시 사용 가능한 쿠폰 조회
codesejin Apr 7, 2024
33459ed
refactor : 쿠폰 사용 가능 검증 여부 리팩토링
codesejin Apr 8, 2024
d85e5a9
feat : 쿠폰 사용 비즈니스 로직 구현
codesejin Apr 8, 2024
c844d64
refactor : list empty체크
codesejin Apr 8, 2024
53c23e2
refactor : 쿠폰 사용 검증 관련 익셉션 처리
codesejin Apr 8, 2024
c36b34d
refactor : 사용 가능한 쿠폰 목록 조회 리팩토링
codesejin Apr 8, 2024
767f1bb
refactor : java doc 추가
codesejin Apr 8, 2024
d8c047f
refactor : 불필요한 코드 제거
codesejin Apr 8, 2024
fca2d3c
refactor : QueryTest @Value사용 방법 적용 못함
codesejin Apr 8, 2024
3c82ef2
refactor : 스키마 수정
codesejin Apr 9, 2024
c1b8be5
refactor : 스키마 수정
codesejin Apr 9, 2024
dc01f8b
refactor : 스키마 수정
codesejin Apr 9, 2024
2b0c15e
chore : properties Value주입 안되서 테스트하느라 직접 명시한거 수정
codesejin Apr 9, 2024
235238a
refactor : 테스트하느라 @Scheduled 주석 처리한거 원복
codesejin Apr 9, 2024
cfbf82b
refactor : 메소드명, 변수명 명확하게 변경
codesejin Apr 9, 2024
450f9d6
refactor : Controller에서 @ResponseStatus삭제
codesejin Apr 15, 2024
894f7c2
refactor : Product Exception 추가 및 가격 타입 decimal변경
codesejin Apr 15, 2024
4e3d989
refactor : 메소드 네이밍 변경 - 쿠폰을 사용했다는 명시적인 네이밍
codesejin Apr 15, 2024
784033c
refactor : 주문 시 사용 가능한 쿠폰 조회 기능 리팩토링
codesejin Apr 15, 2024
d255cd0
refactor : 쿼리에서 로직 지우기
codesejin Apr 15, 2024
51475f9
refactor : 람다 표현식에서 외부 변수 접근 문제 해결
codesejin Apr 15, 2024
f1d6941
refactor : 소나클라우드 이슈 해결
codesejin Apr 15, 2024
c0a419d
refactor : early return & stream API 적용
codesejin Apr 17, 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
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/api/v1/members/signup",
"/main",
"/api/v1/sse/**",
"/api/v1/my-page/**",
"/api/v1/orders/**",
"/api/v1/event/**")
.permitAll()
.requestMatchers(
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/flab/offcoupon/controller/MyPageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.flab.offcoupon.controller;

import com.flab.offcoupon.dto.response.AllCouponsByMemberIdResponse;
import com.flab.offcoupon.service.mypage.MyPageService;
import com.flab.offcoupon.util.ResponseDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RequestMapping("/api/v1/my-page")
@RestController
public class MyPageController {

private final MyPageService myPageService;
@ResponseStatus(HttpStatus.OK)
@GetMapping("/coupons")
public ResponseEntity<ResponseDTO<List<AllCouponsByMemberIdResponse>>> getAllCoupons(@RequestParam final long memberId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

자기자신의 쿠폰 리스트를 확인하는데, 파라미터로 memberId를 받을 필요가 있을까요?
이 유저가 다른 유저의 id를 파라미터로 입력해서 데이터를 조회한다면 어떻게 될까요?

별도의 권한체크가 없으니 parameter manipulation 공격이 성립하겠네요. 이 키워드를 한번 찾아보시고, 취약점을 방어해주세요.

Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 이번 PR에서 수정하지 않고 미루기로했으니, 우선 해당 URL이 오픈되지 않도록 핸들러매핑이 연결되지 않게 @GetMapping("/coupons")을 지워주세요~~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

네엡 다음번 PR에서 수정하도록하겠습니다!

return ResponseEntity.status(HttpStatus.OK).body(myPageService.getAllCoupons(memberId));
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/flab/offcoupon/controller/OrderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.flab.offcoupon.controller;

import com.flab.offcoupon.dto.request.OrderProductRequest;
import com.flab.offcoupon.dto.response.AvailableCouponsByMemberIdResponse;
import com.flab.offcoupon.service.coupon_use.OrderService;
import com.flab.offcoupon.util.ResponseDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

@RequiredArgsConstructor
@RequestMapping("/api/v1/orders")
@RestController
public class OrderController {

private final OrderService orderService;

/**
* 사용 가능한 쿠폰 목록 조회
*
* @param memberId 회원 ID
* @param productId 상품 ID
* @return 사용 가능한 쿠폰 목록
*/
@ResponseStatus(HttpStatus.OK)
Copy link
Collaborator

Choose a reason for hiding this comment

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

ResponseStatus는 사용에 주의해야 합니다. Return type이 ResponseEntity이며, 어노테이션 ResponseStatus를 동시에 쓰는 상황에서 두가지 반환값이 서로 다르다면 어떤 status가 반환될까요?

Copy link
Collaborator Author

@codesejin codesejin Apr 15, 2024

Choose a reason for hiding this comment

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

Controller에서 ResponseEntity와 @ResponseStatus를 동시에 사용할때에는 ResponseEntity에서 명시된 상태코드가 우선됩니다. 즉, ResponseEntity에서 명시된 상태코드가 HTTP 응답으로 반환됩니다.

하지만 응답이 2번 발생할 수 있기 때문에 동시에 2개를 사용하는 것은 권장되지 않고, 아래와 같이 (제가 한 실수,,) 응답코드를 Created와 Ok를 둘 다 사용한다면 다른 개발자가 코드를 읽을때 혼동을 줄 수도 있습니다. 따라서 휴먼 에러를 방지하기 위해 왠만하면 Controller에서 @ResponseStatus를 사용하지 않도록 수정하겠습니다!
image

@GetMapping("/available-coupons")
public ResponseEntity<ResponseDTO<List<AvailableCouponsByMemberIdResponse>>> getAvailableCoupons(@RequestParam final long memberId,
@RequestParam final long productId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분도 마찬가지로, 자기자신의 쿠폰 목록을 조회하는 것인데 memberId를 파라미터로 받을 필요가 있을까요?

LocalDateTime now = LocalDateTime.now();
return ResponseEntity.status(HttpStatus.OK).body(orderService.getAvailableCoupons(memberId, productId, now));
}

/**
* 상품 주문<br>
* 결제 SDK 연동은 현재 진행 중인 프로젝트에서 메인으로 다루지 않기 때문에 제외했습니다.
*
* @param productId 상품 ID
* @param orderProductRequest 주문 요청 정보
* @return 주문 성공 여부
*/
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/products/{productId}")
public ResponseEntity<ResponseDTO<String>> orderProduct(@PathVariable final long productId,
@RequestBody final OrderProductRequest orderProductRequest) {
LocalDateTime now = LocalDateTime.now();
return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.getSuccessResult(orderService.orderProduct(productId,orderProductRequest, now)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public enum CouponStatus {
NOT_ACTIVE("유효기간 시작 전"),
ACTIVE("유효 기간 중"),
USED("사용 완료"),
EXPIRED("기간 완료");
EXPIRED("기간 만료");

private final String description;

Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.flab.offcoupon.domain.entity;

import com.flab.offcoupon.domain.entity.params.AppliedCouponInfo;
import com.flab.offcoupon.domain.entity.params.TimeParams;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.time.LocalDateTime;

/**
* 주문에 사용된 쿠폰 정보를 담는 도메인 객체입니다.
* 하나의 주문에 여러 쿠폰을 사용할 수 있기 때문에 별도의 테이블로 분리하였습니다.
*/
@ToString
@Getter
@AllArgsConstructor
public final class OrderCoupon {
private long id;
private final long orderDetailId;
private final long couponId;
private final long discountAmount;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;

private OrderCoupon(long orderDetailId, AppliedCouponInfo coupon, TimeParams timeParams){
this.orderDetailId = orderDetailId;
this.couponId = coupon.getCouponId();
this.discountAmount = coupon.getDiscountAmount();
this.createdAt = timeParams.createdAt();
this.updatedAt = timeParams.updatedAt();
}

public static OrderCoupon createOrderCoupon(long orderDetailId, AppliedCouponInfo coupon) {
LocalDateTime now = LocalDateTime.now();
return new OrderCoupon(orderDetailId, coupon, new TimeParams(now, now));
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.flab.offcoupon.domain.entity;

import com.flab.offcoupon.domain.entity.params.OrderInfo;
import com.flab.offcoupon.domain.entity.params.TimeParams;
import com.flab.offcoupon.util.DateTimeUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.time.LocalDateTime;

/**
* 주문 상세 정보를 담는 도메인 객체입니다.
*/
@ToString
@Getter
@AllArgsConstructor
public final class OrderDetail {
private long id;
private final long productId;
private final long quantity;
private final long pricePerEach; // 상품 1개당 가격
private final long totalOrderPrice; // 총 상품 주문 가격
private final long totalDiscountPrice; // 할인 가격
private final long totalPaymentPrice; // 총 상품 주문 가격 - 할인 가격
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;

private OrderDetail(OrderInfo orderInfo, TimeParams timeParams) {
this.productId = orderInfo.getProductId();
this.quantity = orderInfo.getQuantity();
this.pricePerEach = orderInfo.getPricePerEach();
this.totalOrderPrice = orderInfo.getTotalOrderPrice();
this.totalDiscountPrice = orderInfo.getTotalDiscountPrice();
this.totalPaymentPrice = orderInfo.getTotalPaymentPrice();
this.createdAt = timeParams.createdAt();
this.updatedAt = timeParams.updatedAt();
}

public static OrderDetail createOrderDetail(OrderInfo orderInfo) {
LocalDateTime now = DateTimeUtils.nowFromZone();
return new OrderDetail(orderInfo, new TimeParams(now, now));
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/flab/offcoupon/domain/entity/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.flab.offcoupon.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.time.LocalDateTime;

/**
* 상품 정보를 담는 도메인 객체입니다.
*/
@ToString
@Getter
@AllArgsConstructor
public final class Product {
private long id;
private final String category;
private final String title;
private final String description;
private final long originalPrice; // 원래 가격
private final Long salePrice; // null일 경우 전체 할인이 적용되지 않은 것으로 간주
Copy link
Collaborator

Choose a reason for hiding this comment

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

null로 비즈니스 로직을 표현하는 것은 NPE의 가능성을 높입니다.
이런 경우 salePrice를 0으로 기본값을 표기해준다면 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

정밀도 향상을 위해 BigDecimal타입으로 변경했고, default 0으로 설정했습니다!

private final long minOrderPrice; // 최소 주문 가격
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.flab.offcoupon.domain.entity.params;

import com.flab.offcoupon.domain.entity.Coupon;
import com.flab.offcoupon.domain.entity.Product;
import com.flab.offcoupon.util.DiscountUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public final class AppliedCouponInfo {

private final long couponId;
private final long discountAmount;

private AppliedCouponInfo(Product product, Coupon coupon) {
this.couponId = coupon.getId();
this.discountAmount = DiscountUtils.calculateDiscountPricePerUnit(product, coupon);
}

public static AppliedCouponInfo createAppliedCouponInfo(Product product, Coupon coupon) {
return new AppliedCouponInfo(product, coupon);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.flab.offcoupon.domain.entity.params;

import com.flab.offcoupon.domain.entity.Coupon;
import com.flab.offcoupon.domain.entity.Product;
import com.flab.offcoupon.util.DiscountUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public final class OrderInfo {
private final long productId;
private final long quantity;
private final long pricePerEach;
private final long totalOrderPrice;
private final List<AppliedCouponInfo> appliedCouponInfos;
private final long totalDiscountPrice;
private final long totalPaymentPrice;

private OrderInfo(Product product, long quantity, List<Coupon> couponList) {
this.productId = product.getId();
this.quantity = quantity;
this.pricePerEach = DiscountUtils.getProductPricePerUnit(product);
this.totalOrderPrice = DiscountUtils.calculateTotalPrice(product, quantity);
this.appliedCouponInfos = createAppliedCouponInfos(product, couponList);
this.totalDiscountPrice = DiscountUtils.calculateTotalDiscountPrice(product, couponList, quantity);
this.totalPaymentPrice = DiscountUtils.calculateTotalPaymentPrice(product, quantity, couponList);
}

public static OrderInfo createOrderInfo(Product product, long quantity, List<Coupon> couponList) {
return new OrderInfo(product, quantity, couponList);
}

private List<AppliedCouponInfo> createAppliedCouponInfos(Product product, List<Coupon> couponList) {
return couponList.stream()
.map(coupon -> AppliedCouponInfo.createAppliedCouponInfo(product, coupon))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.flab.offcoupon.domain.vo.persistence.mypage;

import com.flab.offcoupon.domain.entity.CouponStatus;
import com.flab.offcoupon.domain.entity.DiscountType;

import java.time.LocalDateTime;
/**
* MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO
*
* 마이페이지에서 현재 회원이 가진 모든 쿠폰을 조회하기 위해 사용
*/
public record AllCouponsByMemberIdVo(
long couponId,
String category,
String description,
DiscountType discountType,
Long discountRate,// NULL 일 경우 AMOUNT
Long discountPrice,// NULL 일 경우 PERCENT
LocalDateTime validateStartDate,
LocalDateTime validateEndDate,
CouponStatus couponStatus
)
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.flab.offcoupon.domain.vo.persistence.order;

import com.flab.offcoupon.domain.entity.CouponStatus;
import com.flab.offcoupon.domain.entity.DiscountType;

import java.time.LocalDateTime;

/**
* MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO
* 마이페이지에서 현재 회원이 가진 모든 쿠폰을 조회하기 위해 사용
*/
public record AvailableCouponsByMemberIdVo (
long couponId,
LocalDateTime validateStartDate,
LocalDateTime validateEndDate,
DiscountType discountType,
Long discountRate,// NULL 일 경우 AMOUNT
Long discountPrice,// NULL 일 경우 PERCENT
String category,
String description,
long couponIssueId,
CouponStatus couponStatus,
long discountedPrice // 할인 가격
)
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.flab.offcoupon.domain.vo.persistence.order;

/**
* MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO
* 발급된 쿠폰들의 상태가 활성화인지 확인하기 위해 사용
* @param couponIssueId 쿠폰 발급 ID
* @param isActive 쿠폰의 상태가 활성화인지 여부
*/
public record CouponIssuesAreActiveVo(
long couponIssueId,
boolean isActive

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.flab.offcoupon.domain.vo.persistence.order;

import java.time.LocalDateTime;

/**
* MyBatis에서 여러개의 파라미터를 전달하기 위한 VO
*
* @param memberId
* @param productId
* @param currentDateTime
*/
public record MemberIdProductIdNowVo(
long memberId,
long productId,
LocalDateTime currentDateTime
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.flab.offcoupon.domain.vo.persistence.order;

import java.time.LocalDateTime;

/**
* MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO
* @param couponId 쿠폰 ID
* @param isBetweenValidatePeriod 현재 시간이 쿠폰의 유효기간 범위내에 있는지 여부
*/
public record ValidateNowIsBetweenPeriodVo(
long couponId,
boolean isBetweenValidatePeriod,
LocalDateTime validateStartDate,
LocalDateTime validateEndDate
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.flab.offcoupon.dto.request;

import lombok.Generated;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.List;

@Generated
@Getter
@RequiredArgsConstructor
public final class OrderProductRequest {
private final List<Long> couponIssueId; // 하나의 주문에 여러개의 쿠폰을 사용할 경우
private final List<Long> couponId; // 하나의 주문에 여러개의 쿠폰을 사용할 경우
private final int quantity; // 주문할 상품 수량
}
Loading
Loading