diff --git a/.github/workflows/cd_workflow.yml b/.github/workflows/cd_workflow.yml
index 0916e5c..491276b 100644
--- a/.github/workflows/cd_workflow.yml
+++ b/.github/workflows/cd_workflow.yml
@@ -64,5 +64,5 @@ jobs:
script: |
echo "Starting new application..."
source ~/.bashrc
- nohup java -jar ~/target/BOOK-STORE-COUPON.jar --spring.profiles.active=prod --server.port=${{ secrets.COUPON_PORT }} --spring.datasource.password=${{ secrets.MYSQL_PASSWORD }} --spring.redis.password=${{ secrets.REDIS_PASSWORD }} > coupon.log 2>&1 &
+ nohup java -Xmx256m -jar ~/target/BOOK-STORE-COUPON.jar --spring.profiles.active=prod --server.port=${{ secrets.COUPON_PORT }} --spring.datasource.password=${{ secrets.MYSQL_PASSWORD }} --spring.redis.password=${{ secrets.REDIS_PASSWORD }} > coupon.log 2>&1 &
echo "New application started. Check app.log for details."
diff --git a/pom.xml b/pom.xml
index 1ea5816..2ff00d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,15 +32,6 @@
-
-
-
-
-
-
-
-
-
@@ -61,6 +52,7 @@
spring-boot-starter-web
+
org.springframework.security
spring-security-test
@@ -88,7 +80,11 @@
runtime
-
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
org.springframework.boot
@@ -186,6 +182,20 @@
spring-boot-starter-amqp
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.3.0
+
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/ApplicationStatus.java b/src/main/java/com/nhnacademy/bookstorecoupon/actuator/ApplicationStatus.java
deleted file mode 100644
index 051c750..0000000
--- a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/ApplicationStatus.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.nhnacademy.bookstorecoupon.actuator;
-
-import org.springframework.stereotype.Component;
-
-@Component
-public final class ApplicationStatus {
- private boolean status = true;
-
- public void stopService() {
- this.status = false;
- }
-
- public boolean getStatus() {
- return status;
- }
-}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/CustomHealthIndicator.java b/src/main/java/com/nhnacademy/bookstorecoupon/actuator/CustomHealthIndicator.java
deleted file mode 100644
index f9c30b3..0000000
--- a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/CustomHealthIndicator.java
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-package com.nhnacademy.bookstorecoupon.actuator;
-
-import org.springframework.boot.actuate.health.Health;
-import org.springframework.boot.actuate.health.HealthIndicator;
-import org.springframework.stereotype.Component;
-
-@Component
-public class CustomHealthIndicator implements HealthIndicator {
- private final ApplicationStatus applicationStatus;
-
- public CustomHealthIndicator(ApplicationStatus applicationStatus) {
- this.applicationStatus = applicationStatus;
- }
-
- @Override
- public Health health() {
- if (!applicationStatus.getStatus()) {
- return Health.down().build();
- }
- return Health.up().withDetail("service", "start").build();
- }
-}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/bookcoupon/exception/BookNotFoundException.java b/src/main/java/com/nhnacademy/bookstorecoupon/bookcoupon/exception/BookNotFoundException.java
deleted file mode 100644
index e4ca4d6..0000000
--- a/src/main/java/com/nhnacademy/bookstorecoupon/bookcoupon/exception/BookNotFoundException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.nhnacademy.bookstorecoupon.bookcoupon.exception;
-
-
-import com.nhnacademy.bookstorecoupon.global.exception.GlobalException;
-import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus;
-
-import lombok.Getter;
-
-@Getter
-public class BookNotFoundException extends GlobalException {
- public BookNotFoundException(ErrorStatus errorStatus) {
- super(errorStatus);
- }
-}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/categorycoupon/exception/CategoryNotFoundException.java b/src/main/java/com/nhnacademy/bookstorecoupon/categorycoupon/exception/CategoryNotFoundException.java
deleted file mode 100644
index b4919b9..0000000
--- a/src/main/java/com/nhnacademy/bookstorecoupon/categorycoupon/exception/CategoryNotFoundException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.nhnacademy.bookstorecoupon.categorycoupon.exception;
-
-
-import com.nhnacademy.bookstorecoupon.global.exception.GlobalException;
-import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus;
-
-import lombok.Getter;
-
-@Getter
-public class CategoryNotFoundException extends GlobalException {
- public CategoryNotFoundException(ErrorStatus errorStatus) {
- super(errorStatus);
- }
-}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java
index 42a84b7..a6b71bb 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java
@@ -15,6 +15,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import com.nhnacademy.bookstorecoupon.auth.annotation.AuthorizeRole;
import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyRequestDTO;
import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyUpdateRequestDTO;
import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.response.CouponPolicyResponseDTO;
@@ -22,8 +23,13 @@
import com.nhnacademy.bookstorecoupon.couponpolicy.service.CouponPolicyService;
import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
-
+@Tag(name = "CouponPolicy", description = "쿠폰 정책관련 API")
@RestController
@RequestMapping("/coupons/policies")
public class CouponPolicyController {
@@ -33,24 +39,51 @@ public CouponPolicyController(CouponPolicyService couponPolicyService) {
this.couponPolicyService = couponPolicyService;
}
+ @Operation(
+ summary = "웰컴쿠폰 생성",
+ description = "웰컴쿠폰을 생성합니다"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "웰컴쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PostMapping("/welcome")
- public ResponseEntity issueWelcomeCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
+ public ResponseEntity issueWelcomeCoupon(
+ @Parameter(description = "웰컴쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
validateSaleFields(couponPolicyRequestDTO);
couponPolicyService.issueWelcomeCoupon(couponPolicyRequestDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
-
+ @Operation(
+ summary = "생일쿠폰 생성",
+ description = "웰컴쿠폰을 생성합니다"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "생일쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PostMapping("/birthday")
- public ResponseEntity issueBirthdayCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
+ public ResponseEntity issueBirthdayCoupon(
+ @Parameter(description = "생일쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
validateSaleFields(couponPolicyRequestDTO);
couponPolicyService.issueBirthdayCoupon(couponPolicyRequestDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
+
+ @Operation(summary = "도서 쿠폰 발행", description = "특정 도서에 대한 도서쿠폰을 발행합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "도서쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PostMapping("/books")
- public ResponseEntity issueSpecificBookCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
+ public ResponseEntity issueSpecificBookCoupon(
+ @Parameter(description = "도서쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
validateSaleFields(couponPolicyRequestDTO);
- if (couponPolicyRequestDTO.bookId()==null && couponPolicyRequestDTO.bookTitle() == null){
+ if (couponPolicyRequestDTO.bookId()==null || couponPolicyRequestDTO.bookTitle() == null){
ErrorStatus errorStatus = ErrorStatus.from("북 쿠폰 발행시 책 아이디와 책 제목이 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
throw new CouponPolicyValidationException(errorStatus);
}
@@ -58,10 +91,17 @@ public ResponseEntity issueSpecificBookCoupon(@Valid @RequestBody CouponPo
return ResponseEntity.status(HttpStatus.CREATED).build();
}
+ @Operation(summary = "카테고리 쿠폰 발행", description = "특정 카테고리에 대한 카테고리쿠폰을 발행합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "카테고리쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PostMapping("/categories")
- public ResponseEntity issueSpecificCategoryCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
+ public ResponseEntity issueSpecificCategoryCoupon(
+ @Parameter(description = "카테고리쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
validateSaleFields(couponPolicyRequestDTO);
- if (couponPolicyRequestDTO.categoryId()==null && couponPolicyRequestDTO.categoryName() == null){
+ if (couponPolicyRequestDTO.categoryId()==null || couponPolicyRequestDTO.categoryName() == null){
ErrorStatus errorStatus = ErrorStatus.from("카테고리 쿠폰 발행시 카테고리 아이디와 카테고리 이름이 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
throw new CouponPolicyValidationException(errorStatus);
}
@@ -69,25 +109,47 @@ public ResponseEntity issueSpecificCategoryCoupon(@Valid @RequestBody Coup
return ResponseEntity.status(HttpStatus.CREATED).build();
}
+
+ @Operation(summary = "할인 쿠폰 발행", description = "할인 쿠폰을 발행합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "할인쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PostMapping("/sale")
- public ResponseEntity issueDiscountCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
+ public ResponseEntity issueDiscountCoupon(
+ @Parameter(description = "할인쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) {
validateSaleFields(couponPolicyRequestDTO);
couponPolicyService.issueDiscountCoupon(couponPolicyRequestDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
+ @Operation(summary = "모든 쿠폰 정책 조회", description = "페이징을 통해 모든 쿠폰 정책을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "성공적으로 조회되었습니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@GetMapping
- public ResponseEntity> getAllCouponPolicies(@PageableDefault(page = 1, size = 3) Pageable pageable) {
+ public ResponseEntity> getAllCouponPolicies(@Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 3) Pageable pageable) {
Page policies = couponPolicyService.getAllCouponPolicies(pageable);
return ResponseEntity.status(HttpStatus.OK).body(policies);
}
+ @Operation(summary = "쿠폰 정책 업데이트", description = "쿠폰 정책을 업데이트합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "성공적으로 업데이트되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PatchMapping("/{couponPolicyId}")
- public ResponseEntity updateCouponPolicy(@PathVariable Long couponPolicyId, @Valid @RequestBody CouponPolicyUpdateRequestDTO requestDTO) {
+ public ResponseEntity updateCouponPolicy(
+ @Parameter(description = "쿠폰 정책 아이디", required = true) @PathVariable Long couponPolicyId, @Parameter(description = "쿠폰정책 변경 요청 데이터", required = true) @Valid @RequestBody CouponPolicyUpdateRequestDTO requestDTO) {
if ((requestDTO.salePrice() == null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() == null) ||
- (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)) {
+ (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)||
+ (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() == null)||
+ (requestDTO.salePrice() != null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() != null)) {
ErrorStatus errorStatus = ErrorStatus.from("해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다", HttpStatus.BAD_REQUEST, LocalDateTime.now());
throw new CouponPolicyValidationException(errorStatus);
@@ -100,7 +162,9 @@ public ResponseEntity updateCouponPolicy(@PathVariable Long couponPolicyId
// SalePrice와 SaleRate 유효성 검사 메서드 추가
private void validateSaleFields(CouponPolicyRequestDTO requestDTO) {
if ((requestDTO.salePrice() == null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() == null) ||
- (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)) {
+ (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)||
+ (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() == null)||
+ (requestDTO.salePrice() != null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() != null)) {
ErrorStatus errorStatus = ErrorStatus.from("해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다", HttpStatus.BAD_REQUEST, LocalDateTime.now());
throw new CouponPolicyValidationException(errorStatus);
}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java
index 312bb95..669d945 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java
@@ -2,12 +2,16 @@
import java.math.BigDecimal;
+import jakarta.annotation.Nullable;
+import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
-public record CouponPolicyRequestDTO(@NotNull BigDecimal minOrderPrice,
- BigDecimal salePrice,
- BigDecimal saleRate,
- BigDecimal maxSalePrice,
- @NotBlank String type, Long bookId, String bookTitle, Long categoryId, String categoryName) {
-}
\ No newline at end of file
+
+public record CouponPolicyRequestDTO(@NotNull @Positive @Digits(integer = 8, fraction = 2) BigDecimal minOrderPrice,
+ @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal salePrice,
+ @Nullable @Positive @Digits(integer = 0, fraction = 2) BigDecimal saleRate,
+ @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal maxSalePrice,
+ @NotBlank String type, @Nullable Long bookId, @Nullable String bookTitle, @Nullable Long categoryId, @Nullable String categoryName) {
+}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java
index 2c409c2..d7b4e49 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java
@@ -2,8 +2,12 @@
import java.math.BigDecimal;
+import jakarta.annotation.Nullable;
+import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
-public record CouponPolicyUpdateRequestDTO(@NotNull BigDecimal minOrderPrice, BigDecimal salePrice, BigDecimal saleRate,
- BigDecimal maxSalePrice, @NotNull Boolean isUsed) {
+public record CouponPolicyUpdateRequestDTO(@NotNull @Positive @Digits(integer = 8, fraction = 2) BigDecimal minOrderPrice, @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal salePrice,
+ @Nullable @Positive @Digits(integer = 0, fraction = 2) BigDecimal saleRate,
+ @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal maxSalePrice, @NotNull Boolean isUsed) {
}
\ No newline at end of file
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java
index 5b74518..dff17f1 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java
@@ -20,7 +20,6 @@
import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyUpdateRequestDTO;
import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.response.CouponPolicyResponseDTO;
import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy;
-import com.nhnacademy.bookstorecoupon.couponpolicy.exception.CouponPolicyBanUpdateException;
import com.nhnacademy.bookstorecoupon.couponpolicy.exception.CouponPolicyNotFoundException;
import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository;
import com.nhnacademy.bookstorecoupon.couponpolicy.service.CouponPolicyService;
@@ -108,10 +107,6 @@ public Page getAllCouponPolicies(Pageable pageable) {
public void updateCouponPolicy(Long id, CouponPolicyUpdateRequestDTO requestDTO) {
Optional optionalPolicy = couponPolicyRepository.findById(id);
- if (Boolean.TRUE.equals(requestDTO.isUsed())) {
- ErrorStatus errorStatus = ErrorStatus.from("이미 폐기된 쿠폰정책은 변경할 수 없습니다", HttpStatus.BAD_REQUEST, LocalDateTime.now());
- throw new CouponPolicyBanUpdateException(errorStatus);
- }
if (optionalPolicy.isPresent()) {
CouponPolicy policy = optionalPolicy.get();
@@ -137,6 +132,7 @@ public void updateCouponPolicy(Long id, CouponPolicyUpdateRequestDTO requestDTO)
}
}
+
}
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java
index 1ec59a4..a78c479 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java
@@ -12,12 +12,18 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import com.nhnacademy.bookstorecoupon.auth.annotation.AuthorizeRole;
import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.request.CouponTemplateRequestDTO;
import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.response.CouponTemplateResponseDTO;
import com.nhnacademy.bookstorecoupon.coupontemplate.service.CouponTemplateService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
-
+@Tag(name = "CouponTemplate", description = "쿠폰템플릿관련 API")
@RestController
@RequestMapping("/coupons")
public class CouponTemplateController {
@@ -29,16 +35,27 @@ public CouponTemplateController(CouponTemplateService couponTemplateService) {
this.couponTemplateService = couponTemplateService;
}
+
+ @Operation(summary = "쿠폰 템플릿 생성", description = "새로운 쿠폰 템플릿을 생성합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "쿠폰 템플릿이 성공적으로 생성되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@PostMapping
- public ResponseEntity createCouponTemplate(@RequestBody @Valid CouponTemplateRequestDTO requestDTO) {
+ public ResponseEntity createCouponTemplate(@Parameter(description = "쿠폰 템플릿 발행시 요청데이터", required = true) @Valid @RequestBody CouponTemplateRequestDTO requestDTO) {
couponTemplateService.createCouponTemplate(requestDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
-
+ @Operation(summary = "유저 페이징으로 모든 쿠폰 템플릿 조회", description = "유저 기준으로 페이징된 모든 쿠폰 템플릿을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 템플릿 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
@GetMapping("/issue")
- public ResponseEntity> getAllCouponTemplatesByUserPaging(@PageableDefault(page = 1, size = 3)Pageable pageable) {
+ public ResponseEntity> getAllCouponTemplatesByUserPaging(@Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 3)Pageable pageable) {
Page coupons = couponTemplateService.getAllCouponTemplatesByUserPaging(pageable);
return ResponseEntity.status(HttpStatus.OK).body(coupons);
@@ -46,8 +63,14 @@ public ResponseEntity> getAllCouponTemplatesByUs
}
+ @Operation(summary = "관리자 페이징으로 모든 쿠폰 템플릿 조회", description = "관리자 기준으로 페이징된 모든 쿠폰 템플릿을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 템플릿 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@GetMapping
- public ResponseEntity> getAllCouponTemplatesByManagerPaging(@PageableDefault(page = 1, size = 3) Pageable pageable
+ public ResponseEntity> getAllCouponTemplatesByManagerPaging(@Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 3) Pageable pageable
) {
Page coupons = couponTemplateService.getAllCouponTemplatesByManagerPaging(pageable);
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java
index a26905f..1e3eb90 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java
@@ -7,6 +7,7 @@
public record CouponTemplateRequestDTO(
@NotNull
+ @Positive
Long couponPolicyId,
@NotNull
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java
index e9db841..bd0ae8e 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java
@@ -19,4 +19,5 @@ public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
-}
\ No newline at end of file
+}
+
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java
index be594f7..492df3b 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java
@@ -40,10 +40,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((requests) -> requests
- // .requestMatchers("/api/sign-up").permitAll()
- // .requestMatchers("/api/auth/info").authenticated()
- // .requestMatchers("/api/internal/users/info").permitAll()
- // .requestMatchers("/api/admin").hasRole("ADMIN")
.anyRequest().permitAll()
)
.addFilterBefore(ipAddressFilter, UsernamePasswordAuthenticationFilter.class)
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SwaggerConfig.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SwaggerConfig.java
new file mode 100644
index 0000000..902919f
--- /dev/null
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SwaggerConfig.java
@@ -0,0 +1,31 @@
+package com.nhnacademy.bookstorecoupon.global.config;
+
+import org.springdoc.core.models.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ return new OpenAPI()
+ .info(new Info().title("Coupon API").version("1.0").description("쿠폰 API 명세서"));
+ }
+
+ @Bean
+ public GroupedOpenApi api() {
+ String[] paths = {"/coupons/**"};
+ String[] packagesToScan = {"com.nhnacademy.bookstorecoupon"};
+ return GroupedOpenApi.builder()
+ .group("coupon-api")
+ .pathsToMatch(paths)
+ .packagesToScan(packagesToScan)
+ .build();
+ }
+}
+
+
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerController.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerController.java
new file mode 100644
index 0000000..f62c24a
--- /dev/null
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerController.java
@@ -0,0 +1,40 @@
+package com.nhnacademy.bookstorecoupon.global.controller;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import org.springdoc.webmvc.api.OpenApiResource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("/coupons/api")
+public class SwaggerController {
+
+ @Autowired
+ private OpenApiResource openApiResource;
+
+ @GetMapping()
+ public ResponseEntity getSwaggerJson(HttpServletRequest request) throws JsonProcessingException {
+ String apiDocsUrl = "/coupon-docs/coupon-api"; // Swagger 문서 URL
+ Locale locale = request.getLocale(); // 현재 요청의 Locale
+
+ byte[] swaggerJsonBytes = openApiResource.openapiJson(request, apiDocsUrl, locale);
+ if (swaggerJsonBytes == null) {
+ return ResponseEntity.ok("{}"); // 빈 JSON 반환
+ }
+
+ String swaggerJson = new String(swaggerJsonBytes, StandardCharsets.UTF_8);
+ return ResponseEntity.ok(swaggerJson);
+ }
+}
+
+
+
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerControllerTest.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerControllerTest.java
new file mode 100644
index 0000000..c442d6f
--- /dev/null
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerControllerTest.java
@@ -0,0 +1,41 @@
+package com.nhnacademy.bookstorecoupon.global.controller;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import org.springdoc.webmvc.api.OpenApiResource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+@Controller
+@RequestMapping("/coupons/api-test")
+@Slf4j
+public class SwaggerControllerTest {
+
+ @Autowired
+ private OpenApiResource openApiResource;
+
+ @GetMapping()
+ public String getSwaggerJson(HttpServletRequest request, Model model) throws JsonProcessingException {
+ String apiDocsUrl = "/coupon-docs/coupon-api"; // Swagger 문서 URL
+ Locale locale = request.getLocale(); // 현재 요청의 Locale
+
+ byte[] swaggerJsonBytes = openApiResource.openapiJson(request, apiDocsUrl, locale);
+ if (swaggerJsonBytes == null) {
+ model.addAttribute("swaggerJson", "{}");
+ } else {
+ String swaggerJson = new String(swaggerJsonBytes, StandardCharsets.UTF_8);
+ model.addAttribute("swaggerJson", swaggerJson);
+ }
+
+ return "api/coupon-api";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java b/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java
index 49a97fb..9a1ab67 100644
--- a/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java
+++ b/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java
@@ -18,6 +18,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import com.nhnacademy.bookstorecoupon.auth.annotation.AuthorizeRole;
import com.nhnacademy.bookstorecoupon.auth.annotation.CurrentUser;
import com.nhnacademy.bookstorecoupon.auth.jwt.dto.CurrentUserDetails;
import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus;
@@ -28,8 +29,15 @@
import com.nhnacademy.bookstorecoupon.userandcoupon.service.UserAndCouponService;
import com.nhnacademy.bookstorecoupon.userandcoupon.service.impl.RabbitMQUserAndCouponService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
@RestController
@RequestMapping("/coupons")
+@Tag(name = "UserAndCoupon", description = "사용자 쿠폰관련 API")
public class UserAndCouponController {
private final UserAndCouponService userAndCouponService;
@@ -40,17 +48,27 @@ public UserAndCouponController(UserAndCouponService userAndCouponService, Rabbit
this.rabbitMQUserAndCouponService = rabbitMQUserAndCouponService;
}
+ @Operation(summary = "사용자와 쿠폰 생성", description = "특정 쿠폰 ID로 사용자에게 쿠폰을 발행합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"})
@PostMapping("/{couponId}")
- public ResponseEntity createUserAndCoupon(@PathVariable("couponId") Long couponId, @CurrentUser CurrentUserDetails currentUser) {
+ public ResponseEntity createUserAndCoupon(@Parameter(description = "쿠폰아이디", required = true) @PathVariable("couponId") Long couponId, @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUser) {
Long userId= currentUser.getUserId();
rabbitMQUserAndCouponService.createUserAndCoupon(couponId, userId);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
-
+ @Operation(summary = "웰컴 쿠폰 생성", description = "특정 사용자에게 웰컴 쿠폰을 발행합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "웰컴 쿠폰이 성공적으로 발행되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
@PostMapping("/coupon/welcome")
- public ResponseEntity createUserWelcomeCouponIssue(Long userId) {
+ public ResponseEntity createUserWelcomeCouponIssue(@Parameter(description = "사용자 ID", required = true) Long userId) {
if (userId == null) {
ErrorStatus errorStatus = ErrorStatus.from("유저 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
throw new UserCouponValidationException(errorStatus);
@@ -62,9 +80,14 @@ public ResponseEntity createUserWelcomeCouponIssue(Long userId) {
-
+ @Operation(summary = "마이페이지 쿠폰 조회", description = "특정 사용자 기준으로 페이징된 모든 쿠폰을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"})
@GetMapping("/users/user")
- public ResponseEntity> getAllUserAndCouponsByUserPaging(@CurrentUser CurrentUserDetails currentUser,@PageableDefault(page = 1, size = 3) Pageable pageable) {
+ public ResponseEntity> getAllUserAndCouponsByUserPaging(@Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUser, @Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 4) Pageable pageable) {
Long userId= currentUser.getUserId();
if (userId == null) {
ErrorStatus errorStatus = ErrorStatus.from( "유저 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
@@ -76,11 +99,17 @@ public ResponseEntity> getAllUserAndCouponsByUser
}
+ @Operation(summary = "관리자 기준 페이징된 사용자 쿠폰 조회", description = "관리자 기준으로 페이징된 모든 사용자 쿠폰을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"})
@GetMapping("/users")
public ResponseEntity> getAllUsersAndCouponsByManagerPaging(
- @PageableDefault(page = 1, size = 3) Pageable pageable,
- @RequestParam(required = false) String type,
- @RequestParam(required = false) Long userId
+ @Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 4) Pageable pageable,
+ @Parameter(description = "정책타입", required = false) @RequestParam(required = false) String type,
+ @Parameter(description = "유저 아이디", required = false) @RequestParam(required = false) Long userId
) {
Page coupons = userAndCouponService.getAllUsersAndCouponsByManagerPaging(pageable, type, userId);
@@ -88,13 +117,18 @@ public ResponseEntity> getAllUsersAndCouponsByMan
}
-
+ @Operation(summary = "단건 주문에 대한 쿠폰 조회", description = "특정 주문에 대한 쿠폰을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"})
@GetMapping("/users/order")
public ResponseEntity> findCouponByOrder(
- @CurrentUser CurrentUserDetails currentUserDetails,
- @RequestParam(required = false) List bookIds,
- @RequestParam(required = false) List categoryIds,
- @RequestParam BigDecimal bookPrice) {
+ @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUserDetails,
+ @Parameter(description = "도서 아이디", required = false) @RequestParam(required = false) List bookIds,
+ @Parameter(description = "카테고리 아이디", required = false) @RequestParam(required = false) List categoryIds,
+ @Parameter(description = "도서 가격", required = true) @RequestParam BigDecimal bookPrice) {
if (currentUserDetails.getUserId() == null) {
ErrorStatus errorStatus = ErrorStatus.from("유저 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
@@ -108,11 +142,16 @@ public ResponseEntity> findCouponByOrder(
-
+ @Operation(summary = "장바구니 주문에 대한 쿠폰 조회", description = "특정 장바구니 주문에 대한 쿠폰을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"})
@PostMapping("/users/order/carts")
public ResponseEntity> findCouponByCartOrder(
- @CurrentUser CurrentUserDetails currentUserDetails,
- @RequestBody(required = false) List bookDetails
+ @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUserDetails,
+ @Parameter(description = "도서 주문 정보", required = false) @RequestBody(required = false) List bookDetails
) {
if (currentUserDetails.getUserId() == null) {
@@ -125,9 +164,16 @@ public ResponseEntity> findCouponByCartOrder(
return ResponseEntity.status(HttpStatus.OK).body(coupons);
}
+
+ @Operation(summary = "결제 후 쿠폰 사용됨처리", description = "특정 사용자 쿠폰 ID로 쿠폰을 결제 후 사용됨 처리합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰이 성공적으로 업데이트되었습니다."),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
+ @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"})
@PatchMapping("/users/payment/{userAndCouponId}")
public ResponseEntity updateCouponAfterPayment(
- @PathVariable("userAndCouponId") Long userAndCouponId) {
+ @Parameter(description = "사용자 쿠폰아이디", required = true) @PathVariable("userAndCouponId") Long userAndCouponId) {
if (userAndCouponId == null) {
ErrorStatus errorStatus = ErrorStatus.from("사용자 쿠폰 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
@@ -143,10 +189,15 @@ public ResponseEntity updateCouponAfterPayment(
-
+ @Operation(summary = "선택된 쿠폰 조회", description = "특정 쿠폰 ID로 선택된 쿠폰을 조회합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"),
+ @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.")
+ })
@GetMapping("/users/order/coupon")
+ @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"})
public ResponseEntity getSelectedCoupon(
- @RequestParam(value = "couponId") Long couponId) {
+ @Parameter(description = "쿠폰아이디", required = true) @RequestParam(value = "couponId") Long couponId) {
if (couponId == null) {
ErrorStatus errorStatus = ErrorStatus.from("사용자 쿠폰 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now());
throw new UserCouponValidationException(errorStatus);
@@ -158,9 +209,13 @@ public ResponseEntity getSelectedCoupon(
+ @Operation(summary = "회원인지 아닌지 검사", description = "비회원인지 회원인지 검사합니다.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "검사결과를 true or false로 반환"),
+ })
@GetMapping("/users/auth")
public ResponseEntity isRealUserCheck(
- @CurrentUser CurrentUserDetails currentUserDetails) {
+ @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUserDetails) {
if(currentUserDetails==null){
return ResponseEntity.status(HttpStatus.OK).body(false);
} else {
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/web/ApplicationStatusController.java b/src/main/java/com/nhnacademy/bookstorecoupon/web/ApplicationStatusController.java
deleted file mode 100644
index c3c53de..0000000
--- a/src/main/java/com/nhnacademy/bookstorecoupon/web/ApplicationStatusController.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// package com.nhnacademy.bookstorecoupon.web;
-//
-// import org.springframework.http.HttpStatus;
-// import org.springframework.web.bind.annotation.PostMapping;
-// import org.springframework.web.bind.annotation.RequestMapping;
-// import org.springframework.web.bind.annotation.ResponseStatus;
-// import org.springframework.web.bind.annotation.RestController;
-//
-// import com.netflix.appinfo.ApplicationInfoManager;
-// import com.netflix.appinfo.InstanceInfo;
-// import com.nhnacademy.bookstorecoupon.actuator.ApplicationStatus;
-//
-// @RestController
-// @RequestMapping("/actuator/status")
-// public class ApplicationStatusController {
-//
-// private final ApplicationInfoManager applicationInfoManager;
-// private final ApplicationStatus applicationStatus;
-//
-// public ApplicationStatusController(ApplicationInfoManager applicationInfoManager,
-// ApplicationStatus applicationStatus) {
-// this.applicationInfoManager = applicationInfoManager;
-// this.applicationStatus = applicationStatus;
-// }
-//
-// @PostMapping
-// @ResponseStatus(value = HttpStatus.OK)
-// public void stopStatus() {
-// applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);
-// applicationStatus.stopService();
-// }
-//
-// }
diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/web/HelloController.java b/src/main/java/com/nhnacademy/bookstorecoupon/web/HelloController.java
deleted file mode 100644
index c015ed7..0000000
--- a/src/main/java/com/nhnacademy/bookstorecoupon/web/HelloController.java
+++ /dev/null
@@ -1,22 +0,0 @@
-
-package com.nhnacademy.bookstorecoupon.web;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-
-// @RestController
-// @RequestMapping("/coupon")
-// public class HelloController {
-//
-// @Value("${server.port}")
-// private String port;
-//
-// @GetMapping
-// public String getHello() {
-// return port + ":hello";
-// }
-//
-// }
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 7e7e180..6cc660f 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -36,45 +36,3 @@ logging:
org.springframework.transaction.interceptor: info
remote-addr: 133.186.134.30
-
-# shutdown: graceful
-#
-# eureka:
-# instance:
-# instance-id: coupon
-# hostname: localhost
-# prefer-ip-address: true
-# client:
-# fetch-registry: true
-# register-with-eureka: true
-# service-url:
-# defaultZone: http://admin:1234@localhost:8761/eureka
-#
-# spring:
-# lifecycle:
-# timeout-per-shutdown-phase: 30s
-#
-# management:
-# health:
-# status:
-# order: DOWN, UP
-# endpoint:
-# jolokia:
-# enabled: true
-# metrics:
-# enabled: true
-# pause:
-# enabled: true
-# resume:
-# enabled: true
-# restart:
-# enabled: true
-# shutdown:
-# enabled: true
-# endpoints:
-# web:
-# exposure:
-# include: '*'
-# info:
-# env:
-# enabled: true
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 5d6d778..3116d92 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -31,6 +31,23 @@ spring:
test-on-borrow: true # 풀에서 연결을 빌릴 때마다 해당 연결이 유효한지 검증할지 여부를 설정합니다.
validation-query: SELECT 1 # 연결 유효성을 검사하기 위해 실행할 SQL 쿼리를 설정합니다.
+
+
+springdoc:
+ api-docs:
+ path: /coupon-api
+ default-consumes-media-type: application/json
+ default-produces-media-type: application/json
+ swagger-ui:
+ operations-sorter: alpha
+ tags-sorter: alpha
+ path: /coupon-api.html
+ disable-swagger-default-url: true
+
logging:
file:
- path: ./logs
\ No newline at end of file
+ path: ./logs
+
+
+
+
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index b5196b5..5bed036 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -9,8 +9,8 @@
-
-
+
+
diff --git a/src/main/resources/templates/api/coupon-api.html b/src/main/resources/templates/api/coupon-api.html
new file mode 100644
index 0000000..a76190a
--- /dev/null
+++ b/src/main/resources/templates/api/coupon-api.html
@@ -0,0 +1,41 @@
+
+
+
+
+ Swagger API Documentation
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/bookcoupon/domain/entity/BookCouponTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/bookcoupon/domain/entity/BookCouponTest.java
new file mode 100644
index 0000000..a267a3c
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/bookcoupon/domain/entity/BookCouponTest.java
@@ -0,0 +1,61 @@
+package com.nhnacademy.bookstorecoupon.bookcoupon.domain.entity;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.math.BigDecimal;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.nhnacademy.bookstorecoupon.bookcoupon.repository.BookCouponRepository;
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy;
+import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository;
+import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig;
+
+@DataJpaTest
+@Import(QuerydslConfig.class)
+public class BookCouponTest {
+
+ @Autowired
+ private BookCouponRepository bookCouponRepository;
+
+ @Autowired
+ private CouponPolicyRepository couponPolicyRepository;
+
+ private CouponPolicy couponPolicy;
+
+ @BeforeEach
+ public void setUp() {
+ couponPolicy = CouponPolicy.builder()
+ .minOrderPrice(BigDecimal.valueOf(5000))
+ .salePrice(BigDecimal.valueOf(10000))
+ .saleRate(null)
+ .maxSalePrice(null)
+ .type("book")
+ .build();
+ couponPolicyRepository.save(couponPolicy);
+ }
+
+ @Test
+ public void testCreateBookCoupon() {
+ // Given
+ BookCoupon bookCoupon = BookCoupon.builder()
+ .couponPolicy(couponPolicy)
+ .bookId(123L)
+ .bookTitle("Test Book")
+ .build();
+
+ // When
+ BookCoupon savedBookCoupon = bookCouponRepository.save(bookCoupon);
+
+ // Then
+ assertThat(savedBookCoupon).isNotNull();
+ assertThat(savedBookCoupon.getId()).isNotNull();
+ assertThat(savedBookCoupon.getBookId()).isEqualTo(123L);
+ assertThat(savedBookCoupon.getBookTitle()).isEqualTo("Test Book");
+ assertThat(savedBookCoupon.getCouponPolicy()).isEqualTo(couponPolicy);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/categorycoupon/domain/entity/CategoryCouponTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/categorycoupon/domain/entity/CategoryCouponTest.java
new file mode 100644
index 0000000..e28a86f
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/categorycoupon/domain/entity/CategoryCouponTest.java
@@ -0,0 +1,61 @@
+package com.nhnacademy.bookstorecoupon.categorycoupon.domain.entity;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+
+import java.math.BigDecimal;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.nhnacademy.bookstorecoupon.categorycoupon.repository.CategoryCouponRepository;
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy;
+import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository;
+import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig;
+
+@DataJpaTest
+@Import(QuerydslConfig.class)
+public class CategoryCouponTest {
+
+ @Autowired
+ private CategoryCouponRepository categoryCouponRepository;
+
+ @Autowired
+ private CouponPolicyRepository couponPolicyRepository;
+
+ private CouponPolicy couponPolicy;
+
+ @BeforeEach
+ public void setUp() {
+ couponPolicy = CouponPolicy.builder()
+ .minOrderPrice(BigDecimal.valueOf(5000))
+ .salePrice(BigDecimal.valueOf(10000))
+ .saleRate(null)
+ .maxSalePrice(null)
+ .type("category")
+ .build();
+ couponPolicyRepository.save(couponPolicy);
+ }
+
+ @Test
+ public void testCreateCategoryCoupon() {
+ // Given
+ CategoryCoupon categoryCoupon = CategoryCoupon.builder()
+ .couponPolicy(couponPolicy)
+ .categoryId(456L)
+ .categoryName("Test Category")
+ .build();
+
+ // When
+ CategoryCoupon savedCategoryCoupon = categoryCouponRepository.save(categoryCoupon);
+
+ // Then
+ assertThat(savedCategoryCoupon).isNotNull();
+ assertThat(savedCategoryCoupon.getId()).isNotNull();
+ assertThat(savedCategoryCoupon.getCategoryId()).isEqualTo(456L);
+ assertThat(savedCategoryCoupon.getCategoryName()).isEqualTo("Test Category");
+ assertThat(savedCategoryCoupon.getCouponPolicy()).isEqualTo(couponPolicy);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyControllerTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyControllerTest.java
new file mode 100644
index 0000000..45184cf
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyControllerTest.java
@@ -0,0 +1,439 @@
+package com.nhnacademy.bookstorecoupon.couponpolicy.controller;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
+import static org.springframework.test.util.AssertionErrors.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyRequestDTO;
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyUpdateRequestDTO;
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.response.CouponPolicyResponseDTO;
+import com.nhnacademy.bookstorecoupon.couponpolicy.exception.CouponPolicyValidationException;
+import com.nhnacademy.bookstorecoupon.couponpolicy.service.CouponPolicyService;
+
+/**
+ * @author 이기훈
+ * couponPolicyController 단위테스트
+ */
+@WebMvcTest(CouponPolicyController.class)
+class CouponPolicyControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private CouponPolicyService couponPolicyService;
+
+
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueWelcomeCoupon_Success() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "WELCOME", // type
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/welcome")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+
+ .andExpect(status().isCreated());
+
+ verify(couponPolicyService, times(1)).issueWelcomeCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueWelcomeCoupon_InvalidRequest() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ null, // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "WELCOME", // type
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/welcome")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isBadRequest());
+
+ verify(couponPolicyService, times(0)).issueWelcomeCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueBirthdayCoupon_Success() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(2000), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "BIRTHDAY", // type
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/birthday")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isCreated());
+
+ verify(couponPolicyService, times(1)).issueBirthdayCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueSpecificBookCoupon_Success() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "BOOK", // type
+ 1L, // bookId
+ "Book Title", // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/books")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isCreated());
+
+ verify(couponPolicyService, times(1)).issueSpecificBookCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueSpecificBookCoupon_BookIdOrTitleMissing() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "BOOK", // type
+ 1L, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/books")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isBadRequest());
+ verify(couponPolicyService, times(0)).issueSpecificBookCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueSpecificCategoryCoupon_Success() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "CATEGORY", // type
+ null, // bookId
+ null, // bookTitle
+ 1L, // categoryId
+ "Category Name" // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/categories")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isCreated());
+
+ verify(couponPolicyService, times(1)).issueSpecificCategoryCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueSpecificCategoryCoupon_CategoryIdOrNameMissing() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "CATEGORY", // type
+ null, // bookId
+ null, // bookTitle
+ 1L, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/categories")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isBadRequest());
+
+
+ verify(couponPolicyService, times(0)).issueSpecificCategoryCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueDiscountCoupon_Success() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(5000), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "sale", // type
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/sale")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isCreated());
+
+ verify(couponPolicyService, times(1)).issueDiscountCoupon(any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testGetAllCouponPolicies_Success() throws Exception {
+ CouponPolicyResponseDTO responseDTO = new CouponPolicyResponseDTO(
+ 1L,
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(5000), // salePrice
+ null, // saleRate
+ null, // maxSalePrice
+ "sale",// type
+ true,
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null
+ );
+ Page page = new PageImpl<>(Collections.singletonList(responseDTO));
+
+ when(couponPolicyService.getAllCouponPolicies(any(Pageable.class))).thenReturn(page);
+
+ mockMvc.perform(get("/coupons/policies")
+ .contentType(MediaType.APPLICATION_JSON).with(csrf()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0]").isNotEmpty());
+
+ verify(couponPolicyService, times(1)).getAllCouponPolicies(any(Pageable.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testUpdateCouponPolicy_Success() throws Exception {
+ CouponPolicyUpdateRequestDTO requestDTO = new CouponPolicyUpdateRequestDTO(
+ BigDecimal.valueOf(3000),
+ BigDecimal.valueOf(2000),
+ null,
+ null,
+ true
+ );
+
+ mockMvc.perform(patch("/coupons/policies/1")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isOk());
+
+ verify(couponPolicyService, times(1)).updateCouponPolicy(anyLong(), any(CouponPolicyUpdateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testUpdateCouponPolicy_InvalidRequest() throws Exception {
+ CouponPolicyUpdateRequestDTO requestDTO = new CouponPolicyUpdateRequestDTO(
+ null,
+ BigDecimal.valueOf(2000),
+ null,
+ null,
+ true
+ );
+
+
+ mockMvc.perform(patch("/coupons/policies/1")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO)).with(csrf()))
+ .andExpect(status().isBadRequest());
+
+ verify(couponPolicyService, times(0)).updateCouponPolicy(anyLong(), any(CouponPolicyUpdateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueCoupon_SalePriceAndSaleRateInvalid() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ BigDecimal.valueOf(0.22), // saleRate
+ null, // maxSalePrice
+ "welcome", // type
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/welcome")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO))
+ .with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(result -> {
+ Throwable exception = result.getResolvedException();
+ assertTrue("Expected CouponPolicyValidationException but got: " + exception,
+ exception instanceof CouponPolicyValidationException);
+
+ CouponPolicyValidationException ex = null;
+ if (exception instanceof CouponPolicyValidationException) {
+ ex = (CouponPolicyValidationException) exception;
+ }
+
+ String actualMessage = null;
+ if (ex != null) {
+ actualMessage = ex.getErrorStatus().getMessage();
+ }
+ String expectedMessage = "해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다";
+
+ System.out.println("Actual message: " + actualMessage); // 로그로 확인
+
+ assertTrue("메시지 체크", actualMessage.equals(expectedMessage));
+ });
+
+
+ verify(couponPolicyService, times(0)).issueWelcomeCoupon( any(CouponPolicyRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testIssueCoupon_SalePriceAndMaxSalePriceInvalid() throws Exception {
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ BigDecimal.valueOf(1000), // maxSalePrice
+ "welcome", // type
+ null, // bookId
+ null, // bookTitle
+ null, // categoryId
+ null // categoryName
+ );
+
+ mockMvc.perform(post("/coupons/policies/welcome")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO))
+ .with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(result -> {
+ Throwable exception = result.getResolvedException();
+ assertTrue("Expected CouponPolicyValidationException but got: " + exception,
+ exception instanceof CouponPolicyValidationException);
+
+ CouponPolicyValidationException ex = null;
+ if (exception instanceof CouponPolicyValidationException) {
+ ex = (CouponPolicyValidationException) exception;
+ }
+
+ String actualMessage = null;
+ if (ex != null) {
+ actualMessage = ex.getErrorStatus().getMessage();
+ }
+ String expectedMessage = "해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다";
+
+ System.out.println("Actual message: " + actualMessage); // 로그로 확인
+
+ if (actualMessage != null) {
+ assertTrue("메시지 체크", actualMessage.equals(expectedMessage));
+ }
+ });
+ verify(couponPolicyService, times(0)).issueWelcomeCoupon( any(CouponPolicyRequestDTO.class));
+ }
+
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testUpdateCoupon_SalePriceAndMaxSalePriceSet() throws Exception {
+ CouponPolicyUpdateRequestDTO requestDTO = new CouponPolicyUpdateRequestDTO(
+ BigDecimal.valueOf(1000), // minOrderPrice
+ BigDecimal.valueOf(500), // salePrice
+ null, // saleRate
+ BigDecimal.valueOf(1000),
+ true
+ );
+
+ mockMvc.perform(patch("/coupons/policies/1")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(requestDTO))
+ .with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(result -> {
+ Throwable exception = result.getResolvedException();
+ assertTrue("Expected CouponPolicyValidationException but got: " + exception,
+ exception instanceof CouponPolicyValidationException);
+
+ CouponPolicyValidationException ex = null;
+ if (exception instanceof CouponPolicyValidationException) {
+ ex = (CouponPolicyValidationException) exception;
+ }
+
+ String actualMessage = null;
+ if (ex != null) {
+ actualMessage = ex.getErrorStatus().getMessage();
+ }
+ String expectedMessage = "해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다";
+
+ System.out.println("Actual message: " + actualMessage); // 로그로 확인
+
+ if (actualMessage != null) {
+ assertTrue("메시지 체크", actualMessage.equals(expectedMessage));
+ }
+ });
+
+ verify(couponPolicyService, times(0)).updateCouponPolicy(anyLong(), any(CouponPolicyUpdateRequestDTO.class));
+ }
+
+
+
+
+}
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/entity/CouponPolicyTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/entity/CouponPolicyTest.java
new file mode 100644
index 0000000..a0bd566
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/entity/CouponPolicyTest.java
@@ -0,0 +1,109 @@
+package com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+
+import java.math.BigDecimal;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyRequestDTO;
+import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository;
+import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig;
+
+@DataJpaTest
+@Import(QuerydslConfig.class)
+public class CouponPolicyTest {
+
+ @Autowired
+ private CouponPolicyRepository couponPolicyRepository;
+
+ private CouponPolicy couponPolicy;
+
+ @BeforeEach
+ public void setUp() {
+ couponPolicy = CouponPolicy.builder()
+ .minOrderPrice(BigDecimal.valueOf(5000))
+ .salePrice(null)
+ .saleRate(BigDecimal.valueOf(0.12))
+ .maxSalePrice(BigDecimal.valueOf(20000))
+ .type("sale")
+ .build();
+ couponPolicyRepository.save(couponPolicy);
+ }
+
+ @Test
+ public void testCreateCouponPolicy() {
+ // Given
+ CouponPolicy couponPolicy = CouponPolicy.builder()
+ .minOrderPrice(BigDecimal.valueOf(5000))
+ .salePrice(BigDecimal.valueOf(10000))
+ .saleRate(null)
+ .maxSalePrice(null)
+ .type("welcome")
+ .build();
+
+ // When
+ CouponPolicy savedCouponPolicy = couponPolicyRepository.save(couponPolicy);
+
+ // Then
+ assertThat(savedCouponPolicy).isNotNull();
+ assertThat(savedCouponPolicy.getId()).isNotNull();
+ assertThat(savedCouponPolicy.getMinOrderPrice()).isEqualTo(BigDecimal.valueOf(5000));
+ assertThat(savedCouponPolicy.getSalePrice()).isEqualTo(BigDecimal.valueOf(10000));
+ assertThat(savedCouponPolicy.getSaleRate()).isNull();
+ assertThat(savedCouponPolicy.getMaxSalePrice()).isNull();
+ assertThat(savedCouponPolicy.getType()).isEqualTo("welcome");
+ }
+
+ @Test
+ public void testUpdateCouponPolicy() {
+ // Given
+ BigDecimal newMinOrderPrice = BigDecimal.valueOf(15000);
+ BigDecimal newSaleRate = BigDecimal.valueOf(0.2);
+ BigDecimal newMaxSalePrice = BigDecimal.valueOf(22000);
+ Boolean newIsUsed = false;
+
+ // When
+ couponPolicy.update(newMinOrderPrice, null, newSaleRate, newMaxSalePrice, newIsUsed);
+ CouponPolicy updatedCouponPolicy = couponPolicyRepository.save(couponPolicy);
+
+ // Then
+ assertThat(updatedCouponPolicy).isNotNull();
+ assertThat(updatedCouponPolicy.getMinOrderPrice()).isEqualTo(newMinOrderPrice);
+ assertThat(updatedCouponPolicy.getSalePrice()).isNull();
+ assertThat(updatedCouponPolicy.getSaleRate()).isEqualTo(newSaleRate);
+ assertThat(updatedCouponPolicy.getMaxSalePrice()).isEqualTo(newMaxSalePrice);
+ assertThat(updatedCouponPolicy.getIsUsed()).isEqualTo(newIsUsed);
+ }
+
+ @Test
+ public void testCreateFromRequestDTO() {
+ // Given
+ CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO(
+ BigDecimal.valueOf(5000),
+ BigDecimal.valueOf(10000),
+ null,
+ null,
+ "welcome",
+ null, // Optional field, so it can be null
+ null,
+ null,
+ null
+ );
+
+ // When
+ CouponPolicy createdCouponPolicy = CouponPolicy.createFromRequestDTO(requestDTO);
+
+ // Then
+ assertThat(createdCouponPolicy).isNotNull();
+ assertThat(createdCouponPolicy.getMinOrderPrice()).isEqualTo(requestDTO.minOrderPrice());
+ assertThat(createdCouponPolicy.getSalePrice()).isEqualTo(requestDTO.salePrice());
+ assertThat(createdCouponPolicy.getSaleRate()).isEqualTo(requestDTO.saleRate());
+ assertThat(createdCouponPolicy.getMaxSalePrice()).isEqualTo(requestDTO.maxSalePrice());
+ assertThat(createdCouponPolicy.getType()).isEqualTo(requestDTO.type());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateControllerTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateControllerTest.java
new file mode 100644
index 0000000..e3dd976
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateControllerTest.java
@@ -0,0 +1,255 @@
+package com.nhnacademy.bookstorecoupon.coupontemplate.controller;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.request.CouponTemplateRequestDTO;
+import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.response.CouponTemplateResponseDTO;
+import com.nhnacademy.bookstorecoupon.coupontemplate.service.CouponTemplateService;
+
+@WebMvcTest(CouponTemplateController.class)
+class CouponTemplateControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockBean
+ private CouponTemplateService couponTemplateService;
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testCreateCouponTemplate_Valid() throws Exception {
+ CouponTemplateRequestDTO validRequest = new CouponTemplateRequestDTO(
+ 1L,
+ LocalDateTime.now().plusDays(365),
+ LocalDateTime.now(),
+ 100L
+ );
+
+ mockMvc.perform(post("/coupons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(validRequest)).with(csrf()))
+ .andExpect(status().isCreated());
+
+ verify(couponTemplateService, times(1)).createCouponTemplate(any(CouponTemplateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testCreateCouponTemplate_InvalidCouponPolicyId() throws Exception {
+ CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO(
+ null,
+ LocalDateTime.now().plusDays(1),
+ LocalDateTime.now(),
+ 100L
+ );
+
+ mockMvc.perform(post("/coupons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.message").value("couponPolicyId 에러: must not be null, 입력된 값: null; "))
+ .andExpect(jsonPath("$.status").value("BAD_REQUEST"));
+ verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class));
+
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testCreateCouponTemplate_InvalidExpiredDate() throws Exception {
+ CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO(
+ 1L,
+ null,
+ LocalDateTime.now(),
+ 100L
+ );
+
+ mockMvc.perform(post("/coupons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.message").value("expiredDate 에러: must not be null, 입력된 값: null; "))
+ .andExpect(jsonPath("$.status").value("BAD_REQUEST"));
+
+ verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testCreateCouponTemplate_InvalidIssueDate() throws Exception {
+ CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO(
+ 1L,
+ LocalDateTime.now().plusDays(1),
+ null,
+ 100L
+ );
+
+ mockMvc.perform(post("/coupons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.message").value("issueDate 에러: must not be null, 입력된 값: null; "))
+ .andExpect(jsonPath("$.status").value("BAD_REQUEST"));
+
+ verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testCreateCouponTemplate_InvalidQuantity() throws Exception {
+ CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO(
+ 1L,
+ LocalDateTime.now().plusDays(1),
+ LocalDateTime.now(),
+ null
+ );
+
+ mockMvc.perform(post("/coupons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.message").value("quantity 에러: must not be null, 입력된 값: null; "))
+ .andExpect(jsonPath("$.status").value("BAD_REQUEST"));
+
+ verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"})
+ void testCreateCouponTemplate_NegativeQuantity() throws Exception {
+ CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO(
+ 1L,
+ LocalDateTime.now().plusDays(1),
+ LocalDateTime.now(),
+ -100L
+ );
+
+ mockMvc.perform(post("/coupons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf()))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.message").value("quantity 에러: must be greater than 0, 입력된 값: -100; "))
+ .andExpect(jsonPath("$.status").value("BAD_REQUEST"));
+
+ verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class));
+ }
+
+ @Test
+ @WithMockUser(roles = {"MEMBER"})
+ void testGetAllCouponTemplatesByUserPaging() throws Exception {
+ // CouponTemplateResponseDTO 객체 생성
+ CouponTemplateResponseDTO coupon1 = new CouponTemplateResponseDTO(
+ 1L, 1L, new BigDecimal("100.00"), new BigDecimal("10.00"),
+ null, null, "sale",
+ true, null, null, null, null,
+ LocalDateTime.now().plusDays(30), LocalDateTime.now(), 500L
+ );
+
+ CouponTemplateResponseDTO coupon2 = new CouponTemplateResponseDTO(
+ 2L, 2L, new BigDecimal("200.00"), null,
+ new BigDecimal("0.15"), null, "book",
+ true, 2L, "Another Great Book", null, null,
+ LocalDateTime.now().plusDays(60), LocalDateTime.now().minusDays(5), 300L
+ );
+
+ CouponTemplateResponseDTO coupon3 = new CouponTemplateResponseDTO(
+ 3L, 3L, new BigDecimal("300.00"), new BigDecimal("500.00"),
+ null, null, "category",
+ true, null, null, 3L, "Science",
+ LocalDateTime.now().plusDays(90), LocalDateTime.now().minusDays(10), 200L
+ );
+
+ Page page = new PageImpl<>(List.of(coupon1, coupon2, coupon3));
+
+ Mockito.when(couponTemplateService.getAllCouponTemplatesByUserPaging(Mockito.any(Pageable.class)))
+ .thenReturn(page);
+
+ mockMvc.perform(get("/coupons/issue")
+ .param("page", "0") // 페이지는 0부터 시작하므로 0으로 설정
+ .param("size", "3")
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(csrf()))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.content[0].id").value(1))
+ .andExpect(jsonPath("$.content[0].couponPolicyId").value(1))
+ .andExpect(jsonPath("$.content[0].minOrderPrice").value(100.00))
+ .andExpect(jsonPath("$.content[1].id").value(2))
+ .andExpect(jsonPath("$.content[1].couponPolicyId").value(2))
+ .andExpect(jsonPath("$.content[1].minOrderPrice").value(200.00))
+ .andExpect(jsonPath("$.content[2].id").value(3))
+ .andExpect(jsonPath("$.content[2].couponPolicyId").value(3))
+ .andExpect(jsonPath("$.content[2].minOrderPrice").value(300.00));
+ }
+
+ @Test
+ @WithMockUser(roles = {"COUPON_ADMIN"}) // 올바른 역할로 설정
+ void testGetAllCouponTemplatesByManagerPaging() throws Exception {
+ // CouponTemplateResponseDTO 객체 생성
+ CouponTemplateResponseDTO coupon1 = new CouponTemplateResponseDTO(
+ 1L, 1L, new BigDecimal("100.00"), new BigDecimal("10.00"),
+ null, null, "sale",
+ true, null, null, null, null,
+ LocalDateTime.now().plusDays(30), LocalDateTime.now(), 500L
+ );
+
+ CouponTemplateResponseDTO coupon2 = new CouponTemplateResponseDTO(
+ 2L, 2L, new BigDecimal("200.00"), null,
+ new BigDecimal("0.15"), null, "book",
+ true, 2L, "Another Great Book", null, null,
+ LocalDateTime.now().plusDays(60), LocalDateTime.now().minusDays(5), 300L
+ );
+
+ CouponTemplateResponseDTO coupon3 = new CouponTemplateResponseDTO(
+ 3L, 3L, new BigDecimal("300.00"), new BigDecimal("500.00"),
+ null, null, "category",
+ true, null, null, 3L, "Science",
+ LocalDateTime.now().plusDays(90), LocalDateTime.now().minusDays(10), 200L
+ );
+
+ Page page = new PageImpl<>(List.of(coupon1, coupon2, coupon3));
+
+ Mockito.when(couponTemplateService.getAllCouponTemplatesByManagerPaging(Mockito.any(Pageable.class)))
+ .thenReturn(page);
+
+ mockMvc.perform(get("/coupons")
+ .param("page", "0") // 페이지는 0부터 시작하므로 0으로 설정
+ .param("size", "2")
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(csrf())) // CSRF 토큰을 포함
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.content[0].id").value(1))
+ .andExpect(jsonPath("$.content[0].couponPolicyId").value(1))
+ .andExpect(jsonPath("$.content[0].minOrderPrice").value(100.00))
+ .andExpect(jsonPath("$.content[1].id").value(2))
+ .andExpect(jsonPath("$.content[1].couponPolicyId").value(2))
+ .andExpect(jsonPath("$.content[1].minOrderPrice").value(200.00))
+ .andExpect(jsonPath("$.content[2].id").value(3))
+ .andExpect(jsonPath("$.content[2].couponPolicyId").value(3))
+ .andExpect(jsonPath("$.content[2].minOrderPrice").value(300.00));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/entity/CouponTemplateTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/entity/CouponTemplateTest.java
new file mode 100644
index 0000000..673a909
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/entity/CouponTemplateTest.java
@@ -0,0 +1,85 @@
+package com.nhnacademy.bookstorecoupon.coupontemplate.domain.entity;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy;
+import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository;
+import com.nhnacademy.bookstorecoupon.coupontemplate.repository.CouponTemplateRepository;
+import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig;
+
+@DataJpaTest
+@Import(QuerydslConfig.class)
+public class CouponTemplateTest {
+
+ @Autowired
+ private CouponTemplateRepository couponTemplateRepository;
+
+ @Autowired
+ private CouponPolicyRepository couponPolicyRepository;
+
+ private CouponPolicy couponPolicy;
+
+ @BeforeEach
+ public void setUp() {
+ couponPolicy = CouponPolicy.builder()
+ .minOrderPrice(BigDecimal.valueOf(5000))
+ .salePrice(BigDecimal.valueOf(10000))
+ .saleRate(null)
+ .maxSalePrice(null)
+ .type("welcome")
+ .build();
+ couponPolicyRepository.save(couponPolicy);
+ }
+
+ @Test
+ public void testCreateCouponTemplate() {
+ // Given
+ LocalDateTime now = LocalDateTime.now();
+ CouponTemplate couponTemplate = CouponTemplate.builder()
+ .couponPolicy(couponPolicy)
+ .expiredDate(now.plusDays(30))
+ .issueDate(now)
+ .quantity(100L)
+ .build();
+
+ // When
+ CouponTemplate savedCouponTemplate = couponTemplateRepository.save(couponTemplate);
+
+ // Then
+ assertThat(savedCouponTemplate).isNotNull();
+ assertThat(savedCouponTemplate.getId()).isNotNull();
+ assertThat(savedCouponTemplate.getCouponPolicy()).isEqualTo(couponPolicy);
+ assertThat(savedCouponTemplate.getExpiredDate()).isEqualTo(now.plusDays(30));
+ assertThat(savedCouponTemplate.getIssueDate()).isEqualTo(now);
+ assertThat(savedCouponTemplate.getQuantity()).isEqualTo(100L);
+ }
+
+ @Test
+ public void testUpdateCouponTemplate() {
+ // Given
+ LocalDateTime now = LocalDateTime.now();
+ CouponTemplate couponTemplate = CouponTemplate.builder()
+ .couponPolicy(couponPolicy)
+ .expiredDate(now.plusDays(30))
+ .issueDate(now)
+ .quantity(100L)
+ .build();
+ CouponTemplate savedCouponTemplate = couponTemplateRepository.save(couponTemplate);
+
+ // When
+ savedCouponTemplate.update(200L);
+
+ // Then
+ assertThat(savedCouponTemplate).isNotNull();
+ assertThat(savedCouponTemplate.getQuantity()).isEqualTo(200L);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponControllerTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponControllerTest.java
new file mode 100644
index 0000000..65b4256
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponControllerTest.java
@@ -0,0 +1,214 @@
+// package com.nhnacademy.bookstorecoupon.userandcoupon.controller;
+//
+// import static org.mockito.Mockito.*;
+// import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
+//
+// import org.junit.jupiter.api.Test;
+// import org.springframework.beans.factory.annotation.Autowired;
+// import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+// import org.springframework.boot.test.mock.mockito.MockBean;
+// import org.springframework.http.MediaType;
+// import org.springframework.security.test.context.support.WithMockUser;
+// import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+// import org.springframework.test.web.servlet.MockMvc;
+// import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+// import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+//
+// import com.fasterxml.jackson.databind.ObjectMapper;
+// import com.nhnacademy.bookstorecoupon.auth.jwt.dto.CurrentUserDetails;
+// import com.nhnacademy.bookstorecoupon.userandcoupon.service.UserAndCouponService;
+// import com.nhnacademy.bookstorecoupon.userandcoupon.service.impl.RabbitMQUserAndCouponService;
+//
+// @WebMvcTest(UserAndCouponController.class)
+// class UserAndCouponControllerTest {
+//
+//
+// @Autowired
+// private MockMvc mockMvc;
+//
+//
+// @MockBean
+// private UserAndCouponService userAndCouponService;
+//
+// @MockBean
+// private RabbitMQUserAndCouponService rabbitMQUserAndCouponService;
+//
+// @MockBean
+// private CurrentUserDetails currentUserDetails;
+//
+// @Autowired
+// private ObjectMapper objectMapper;
+//
+//
+// @Test
+// @WithMockUser(roles = {"MEMBER"})
+// void createUserAndCoupon_ShouldReturnCreated_WhenValidRequest() throws Exception {
+// Long couponId = 1L;
+// Long userId = 1L;
+//
+//
+// // Mock the currentUserDetails to return the userId
+// when(currentUserDetails.getUserId()).thenReturn(userId);
+//
+// // Perform the request
+// mockMvc.perform(MockMvcRequestBuilders.post("/coupons/{couponId}", couponId)
+// .with(SecurityMockMvcRequestPostProcessors.user("user").roles("MEMBER"))
+// .contentType(MediaType.APPLICATION_JSON)
+// .with(csrf())) // csrf token이 필요한 경우
+// .andExpect(MockMvcResultMatchers.status().isCreated());
+//
+// // Verify that the RabbitMQUserAndCouponService was called exactly once
+// verify(rabbitMQUserAndCouponService, times(1)).createUserAndCoupon(couponId, userId);
+// }
+// }
+//
+// // @Test
+// // void createUserWelcomeCouponIssue_ShouldReturnCreated_WhenValidRequest() {
+// // Long userId = 1L;
+// //
+// // ResponseEntity response = userAndCouponController.createUserWelcomeCouponIssue(userId);
+// //
+// // verify(userAndCouponService).createUserWelcomeCouponIssue(userId);
+// // assertEquals(HttpStatus.CREATED, response.getStatusCode());
+// // }
+// //
+// // @Test
+// // void createUserWelcomeCouponIssue_ShouldThrowException_WhenUserIdIsNull() {
+// // Long userId = null;
+// //
+// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () ->
+// // userAndCouponController.createUserWelcomeCouponIssue(userId));
+// //
+// // assertEquals("유저 아이디가 필요합니다.", thrown.getMessage());
+// // }
+// //
+// // @Test
+// // void getAllUserAndCouponsByUserPaging_ShouldReturnPageOfCoupons() {
+// // Long userId = 1L;
+// // Pageable pageable = PageRequest.of(0, 10);
+// // Page page = new PageImpl<>(List.of(new UserAndCouponResponseDTO()));
+// //
+// // when(currentUserDetails.getUserId()).thenReturn(userId);
+// // when(userAndCouponService.getAllUsersAndCouponsByUserPaging(userId, pageable)).thenReturn(page);
+// //
+// // ResponseEntity> response = userAndCouponController.getAllUserAndCouponsByUserPaging(currentUserDetails, pageable);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertEquals(1, response.getBody().getTotalElements());
+// // }
+// //
+// // @Test
+// // void getAllUserAndCouponsByUserPaging_ShouldThrowException_WhenUserIdIsNull() {
+// // when(currentUserDetails.getUserId()).thenReturn(null);
+// //
+// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () ->
+// // userAndCouponController.getAllUserAndCouponsByUserPaging(currentUserDetails, PageRequest.of(0, 10)));
+// //
+// // assertEquals("유저 아이디가 필요합니다.", thrown.getMessage());
+// // }
+// //
+// // @Test
+// // void getAllUsersAndCouponsByManagerPaging_ShouldReturnPageOfCoupons() {
+// // Pageable pageable = PageRequest.of(0, 10);
+// // Page page = new PageImpl<>(List.of(new UserAndCouponResponseDTO()));
+// //
+// // when(userAndCouponService.getAllUsersAndCouponsByManagerPaging(pageable, null, null)).thenReturn(page);
+// //
+// // ResponseEntity> response = userAndCouponController.getAllUsersAndCouponsByManagerPaging(pageable, null, null);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertEquals(1, response.getBody().getTotalElements());
+// // }
+// //
+// // @Test
+// // void findCouponByOrder_ShouldReturnListOfCoupons() {
+// // Long userId = 1L;
+// // BigDecimal bookPrice = BigDecimal.valueOf(100);
+// // List coupons = List.of(new UserAndCouponResponseDTO());
+// //
+// // when(currentUserDetails.getUserId()).thenReturn(userId);
+// // when(userAndCouponService.findCouponByOrder(userId, null, null, bookPrice)).thenReturn(coupons);
+// //
+// // ResponseEntity> response = userAndCouponController.findCouponByOrder(currentUserDetails, null, null, bookPrice);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertEquals(coupons, response.getBody());
+// // }
+// //
+// // @Test
+// // void findCouponByCartOrder_ShouldReturnListOfCoupons() {
+// // Long userId = 1L;
+// // List bookDetails = List.of(new GetBookByOrderCouponResponse());
+// // List coupons = List.of(new UserAndCouponResponseDTO());
+// //
+// // when(currentUserDetails.getUserId()).thenReturn(userId);
+// // when(userAndCouponService.findCouponByCartOrder(userId, bookDetails)).thenReturn(coupons);
+// //
+// // ResponseEntity> response = userAndCouponController.findCouponByCartOrder(currentUserDetails, bookDetails);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertEquals(coupons, response.getBody());
+// // }
+// //
+// // @Test
+// // void updateCouponAfterPayment_ShouldReturnOk_WhenValidRequest() {
+// // Long userAndCouponId = 1L;
+// //
+// // ResponseEntity response = userAndCouponController.updateCouponAfterPayment(userAndCouponId);
+// //
+// // verify(userAndCouponService).updateCouponAfterPayment(userAndCouponId);
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // }
+// //
+// // @Test
+// // void updateCouponAfterPayment_ShouldThrowException_WhenUserAndCouponIdIsNull() {
+// // Long userAndCouponId = null;
+// //
+// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () ->
+// // userAndCouponController.updateCouponAfterPayment(userAndCouponId));
+// //
+// // assertEquals("사용자 쿠폰 아이디가 필요합니다.", thrown.getMessage());
+// // }
+// //
+// // @Test
+// // void getSelectedCoupon_ShouldReturnCoupon_WhenValidRequest() {
+// // Long couponId = 1L;
+// // UserAndCouponOrderResponseDTO coupon = new UserAndCouponOrderResponseDTO();
+// //
+// // when(userAndCouponService.findUserAndCouponsById(couponId)).thenReturn(coupon);
+// //
+// // ResponseEntity response = userAndCouponController.getSelectedCoupon(couponId);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertEquals(coupon, response.getBody());
+// // }
+// //
+// // @Test
+// // void getSelectedCoupon_ShouldThrowException_WhenCouponIdIsNull() {
+// // Long couponId = null;
+// //
+// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () ->
+// // userAndCouponController.getSelectedCoupon(couponId));
+// //
+// // assertEquals("사용자 쿠폰 아이디가 필요합니다.", thrown.getMessage());
+// // }
+//
+// // @Test
+// // void isRealUserCheck_ShouldReturnTrue_WhenUserIsReal() {
+// // when(currentUserDetails).thenReturn(new CurrentUserDetails()); // Non-null CurrentUserDetails
+// //
+// // ResponseEntity response = userAndCouponController.isRealUserCheck(currentUserDetails);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertTrue(response.getBody());
+// // }
+// //
+// // @Test
+// // void isRealUserCheck_ShouldReturnFalse_WhenUserIsNull() {
+// // when(currentUserDetails).thenReturn(null);
+// //
+// // ResponseEntity response = userAndCouponController.isRealUserCheck(null);
+// //
+// // assertEquals(HttpStatus.OK, response.getStatusCode());
+// // assertFalse(response.getBody());
+// // }
diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/domain/entity/UserAndCouponTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/domain/entity/UserAndCouponTest.java
new file mode 100644
index 0000000..fa2e73a
--- /dev/null
+++ b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/domain/entity/UserAndCouponTest.java
@@ -0,0 +1,93 @@
+package com.nhnacademy.bookstorecoupon.userandcoupon.domain.entity;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy;
+import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository;
+import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig;
+import com.nhnacademy.bookstorecoupon.userandcoupon.repository.UserAndCouponRepository;
+
+@DataJpaTest
+@Import(QuerydslConfig.class)
+public class UserAndCouponTest {
+
+ @Autowired
+ private UserAndCouponRepository userAndCouponRepository;
+
+ @Autowired
+ private CouponPolicyRepository couponPolicyRepository;
+
+ private CouponPolicy couponPolicy;
+
+ @BeforeEach
+ public void setUp() {
+ couponPolicy = CouponPolicy.builder()
+ .minOrderPrice(BigDecimal.valueOf(5000))
+ .salePrice(BigDecimal.valueOf(10000))
+ .saleRate(null)
+ .maxSalePrice(null)
+ .type("welcome")
+ .build();
+ couponPolicyRepository.save(couponPolicy);
+ }
+
+ @Test
+ public void testCreateUserAndCoupon() {
+ // Given
+ UserAndCoupon userAndCoupon = UserAndCoupon.builder()
+ .couponPolicy(couponPolicy)
+ .userId(123L)
+ .usedDate(null)
+ .isUsed(false)
+ .expiredDate(LocalDateTime.of(2024, 12, 31, 23, 59, 59))
+ .issueDate(LocalDateTime.now())
+ .build();
+
+ // When
+ UserAndCoupon savedUserAndCoupon = userAndCouponRepository.save(userAndCoupon);
+
+ // Then
+ assertThat(savedUserAndCoupon).isNotNull();
+ assertThat(savedUserAndCoupon.getId()).isNotNull();
+ assertThat(savedUserAndCoupon.getCouponPolicy()).isEqualTo(couponPolicy);
+ assertThat(savedUserAndCoupon.getUserId()).isEqualTo(123L);
+ assertThat(savedUserAndCoupon.getUsedDate()).isNull();
+ assertThat(savedUserAndCoupon.getIsUsed()).isFalse();
+ assertThat(savedUserAndCoupon.getExpiredDate()).isEqualTo(LocalDateTime.of(2024, 12, 31, 23, 59, 59));
+ assertThat(savedUserAndCoupon.getIssueDate()).isNotNull();
+ }
+
+ @Test
+ public void testUpdateUserAndCoupon() {
+ // Given
+ UserAndCoupon userAndCoupon = UserAndCoupon.builder()
+ .couponPolicy(couponPolicy)
+ .userId(123L)
+ .usedDate(null)
+ .isUsed(false)
+ .expiredDate(LocalDateTime.of(2024, 12, 31, 23, 59, 59))
+ .issueDate(LocalDateTime.now())
+ .build();
+ UserAndCoupon savedUserAndCoupon = userAndCouponRepository.save(userAndCoupon);
+
+ LocalDateTime newUsedDate = LocalDateTime.now();
+ Boolean newIsUsed = true;
+
+ // When
+ savedUserAndCoupon.update(newUsedDate, newIsUsed);
+
+ // Then
+ assertThat(savedUserAndCoupon).isNotNull();
+ assertThat(savedUserAndCoupon.getUsedDate()).isEqualTo(newUsedDate);
+ assertThat(savedUserAndCoupon.getIsUsed()).isTrue();
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
index 9580ce1..d93dfe7 100644
--- a/src/test/resources/application.yml
+++ b/src/test/resources/application.yml
@@ -13,10 +13,18 @@ spring:
refresh-token:
expires-in: 86400000
datasource:
- url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
+ url: jdbc:h2:mem:testdb;MODE=MYSQL;
driver-class-name: org.h2.Driver
username: sa
- password:
+ type: org.apache.commons.dbcp2.BasicDataSource
+ dbcp2:
+ initial-size: 50 # 초기로 생성할 연결의 수를 설정합니다.
+ max-total: 50 # 풀에서 유지할 수 있는 최대 연결 수를 설정합니다.
+ max-idle: 50 # 유휴 상태로 유지할 최대 연결 수를 설정합니다.
+ min-idle: 50 # 유휴 상태로 유지할 최소 연결 수를 설정합니다.
+ test-on-borrow: true # 풀에서 연결을 빌릴 때마다 해당 연결이 유효한지 검증할지 여부를 설정합니다.
+ validation-query: SELECT 1 # 연결 유효성을 검사하기 위해 실행할 SQL 쿼리를 설정합니다.
+
jpa:
hibernate:
ddl-auto: update
@@ -27,3 +35,5 @@ spring:
remote-addr: 127.0.0.1
+
+