From f1fe1517106a44df702845314f8a14a2d0d17495 Mon Sep 17 00:00:00 2001 From: sejin park <101460733+codesejin@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:11:00 +0900 Subject: [PATCH 01/27] =?UTF-8?q?docs=20:=20README.md=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 05864f2..e408477 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # off-coupon 선착순 쿠폰 발행 서비스 입니다. (네고왕 이벤트 참고) + + +## Git Flow + +✅ master : 릴리스 버전을 관리하는 메인 브랜치 +✅ develop : 개발이 진행되는 통합 브랜치 +✅ feature : 새로운 기능을 개발하는 브랜치 +✅ release : 다가오는 릴리스를 준비하는 브랜치 +✅ hotfix : 실제 프로덕션에서 발생한 버그를 수정하는 브랜치 + +Reference : [우아한 형제들 기술블로그 : gitFlow](https://techblog.woowahan.com/2553/) + +## Commit Convention + +✅ feat : 새로운 기능 추가 +✅ fix : 버그 수정 +✅ docs : 문서 수정 +✅ style : 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우 +✅ refactor : 코드 리팩토링 +✅ test : 테스트 코드, 리팩토링 테스트 코드 +✅ chore : 빌드 업무 수정, 패키지 매니저 수정 From 02691a4e2530f54ce1d95fe9bad7148ad8ae0e23 Mon Sep 17 00:00:00 2001 From: sejin park <101460733+codesejin@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:11:32 +0900 Subject: [PATCH 02/27] docs : Update README.md --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e408477..96b2bf6 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,20 @@ ## Git Flow -✅ master : 릴리스 버전을 관리하는 메인 브랜치 -✅ develop : 개발이 진행되는 통합 브랜치 -✅ feature : 새로운 기능을 개발하는 브랜치 -✅ release : 다가오는 릴리스를 준비하는 브랜치 -✅ hotfix : 실제 프로덕션에서 발생한 버그를 수정하는 브랜치 +✅ master : 릴리스 버전을 관리하는 메인 브랜치 +✅ develop : 개발이 진행되는 통합 브랜치 +✅ feature : 새로운 기능을 개발하는 브랜치 +✅ release : 다가오는 릴리스를 준비하는 브랜치 +✅ hotfix : 실제 프로덕션에서 발생한 버그를 수정하는 브랜치 Reference : [우아한 형제들 기술블로그 : gitFlow](https://techblog.woowahan.com/2553/) ## Commit Convention -✅ feat : 새로운 기능 추가 -✅ fix : 버그 수정 -✅ docs : 문서 수정 -✅ style : 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우 -✅ refactor : 코드 리팩토링 -✅ test : 테스트 코드, 리팩토링 테스트 코드 -✅ chore : 빌드 업무 수정, 패키지 매니저 수정 +✅ feat : 새로운 기능 추가 +✅ fix : 버그 수정 +✅ docs : 문서 수정 +✅ style : 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우 +✅ refactor : 코드 리팩토링 +✅ test : 테스트 코드, 리팩토링 테스트 코드 +✅ chore : 빌드 업무 수정, 패키지 매니저 수정 From 8d2ad96f11cc6c5b9ca06ab92f460cf5133e0bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F-Lab=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0=20=EB=B4=87?= <155365540+f-lab-bot@users.noreply.github.com> Date: Thu, 25 Jan 2024 05:54:52 +0900 Subject: [PATCH 03/27] =?UTF-8?q?=EC=9E=90=EB=8F=99=20PR=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20workflow=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80=20by=20f-lab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sonarcloud-analyze.yml | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/sonarcloud-analyze.yml diff --git a/.github/workflows/sonarcloud-analyze.yml b/.github/workflows/sonarcloud-analyze.yml new file mode 100644 index 0000000..962f534 --- /dev/null +++ b/.github/workflows/sonarcloud-analyze.yml @@ -0,0 +1,63 @@ +name: F-Lab SonarCloud Code Analyze + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +env: + CACHED_DEPENDENCIES_PATHS: '**/node_modules' + +jobs: + CodeAnalyze: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set SonarCloud Project Key + run: | + REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 2) + ORG_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 1) + SONAR_PROJECT_KEY="${ORG_NAME}_${REPO_NAME}" + echo "SONAR_PROJECT_KEY=$SONAR_PROJECT_KEY" >> $GITHUB_ENV + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + java-version: '19' + distribution: 'adopt' + + - name: Create Sonar Gradle File + run: | + insert_string="plugins { id 'org.sonarqube' version '4.4.1.3373' }" + if [ -f "build.gradle" ]; then + echo "$insert_string" > temp.gradle + cat build.gradle >> temp.gradle + echo "" >> temp.gradle + echo "sonarqube {" >> temp.gradle + echo " properties {" >> temp.gradle + echo " property 'sonar.java.binaries', '**'" >> temp.gradle + echo " }" >> temp.gradle + echo "}" >> temp.gradle + mv temp.gradle build.gradle + else + echo "$insert_string" > build.gradle + echo "" >> build.gradle + echo "sonarqube {" >> build.gradle + echo " properties {" >> build.gradle + echo " property 'sonar.java.binaries', '**'" >> build.gradle + echo " }" >> build.gradle + echo "}" >> build.gradle + fi + chmod 777 ./gradlew + + + - name: Analyze + run: ./gradlew sonar -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} -Dsonar.organization=f-lab-edu-1 -Dsonar.host.url=https://sonarcloud.io -Dsonar.token=${{ secrets.SECRET_SONARQUBE }} -Dsonar.gradle.skipCompile=true + env: + SONAR_TOKEN: ${{ secrets.SECRET_SONARQUBE }} + + \ No newline at end of file From c9d9a2f09b759da1bd090eab56060e9d78931d8b Mon Sep 17 00:00:00 2001 From: sejin park Date: Sat, 27 Jan 2024 17:45:08 +0900 Subject: [PATCH 04/27] =?UTF-8?q?chore=20:=20"JPA"=EC=97=90=EC=84=9C=20"My?= =?UTF-8?q?batis"=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subquery, Join 등 코드 리뷰를 더 세심하게 받기 위함 --- build.gradle | 4 ++- .../flab/offcoupon/OffcouponApplication.java | 2 +- .../java/com/flab/offcoupon/TestRunner.java | 29 +++++++++++++++++++ src/main/resources/application.properties | 21 ++++++++++++++ src/main/resources/data.sql | 3 ++ src/main/resources/schema.sql | 9 ++++++ 6 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/flab/offcoupon/TestRunner.java create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql diff --git a/build.gradle b/build.gradle index 8b7e983..be89bed 100644 --- a/build.gradle +++ b/build.gradle @@ -22,13 +22,15 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + } tasks.named('test') { diff --git a/src/main/java/com/flab/offcoupon/OffcouponApplication.java b/src/main/java/com/flab/offcoupon/OffcouponApplication.java index 12f4077..e06af2c 100644 --- a/src/main/java/com/flab/offcoupon/OffcouponApplication.java +++ b/src/main/java/com/flab/offcoupon/OffcouponApplication.java @@ -1,5 +1,6 @@ package com.flab.offcoupon; +import jakarta.servlet.http.HttpSession; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -9,5 +10,4 @@ public class OffcouponApplication { public static void main(String[] args) { SpringApplication.run(OffcouponApplication.class, args); } - } diff --git a/src/main/java/com/flab/offcoupon/TestRunner.java b/src/main/java/com/flab/offcoupon/TestRunner.java new file mode 100644 index 0000000..394efbd --- /dev/null +++ b/src/main/java/com/flab/offcoupon/TestRunner.java @@ -0,0 +1,29 @@ +package com.flab.offcoupon; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TestRunner implements ApplicationRunner { + + private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; + @Override + public void run(ApplicationArguments args) throws Exception { + Connection connection = dataSource.getConnection(); + log.info("DBCP:{}", dataSource.getClass()); + log.info("Url:{}", connection.getMetaData().getURL()); + log.info("username:{}", connection.getMetaData().getUserName()); + + jdbcTemplate.execute("INSERT INTO Member(name, password, email) VALUES ('Lion', '1234', 'lion@naver.com')"); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..94ed2b8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,22 @@ +server.port=8080 + +# h2 +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# H2 Database Configuration +spring.datasource.url=jdbc:h2:mem:mybatis-test +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.datasource.hikari.maximum-pool-size=4 +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# MyBatis Configuration +# mapper.xml ?? ?? +mybatis.mapper-locations=classpath:mapper/**/*.xml +# camel case ?? +mybatis.configuration.map-underscore-to-camel-case=true +# package model ?? ?? +mybatis.type-aliases-package=com.flab.offcoupon diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..28da063 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,3 @@ +INSERT INTO Member(name, password, email) VALUES ('SEJIN1', '1234','seijin@naver.com') +INSERT INTO Member(name, password, email) VALUES ('SEJIN2', '1234','seijin@naver.com') +INSERT INTO Member(name, password, email) VALUES ('SEJIN3', '1234','seijin@naver.com') \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..df6da10 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS Member; + +CREATE TABLE Member +( + id IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL +); \ No newline at end of file From 7121a2a27080b4772d295d8c5fb56211a3d5df8e Mon Sep 17 00:00:00 2001 From: sejin park <101460733+codesejin@users.noreply.github.com> Date: Sun, 28 Jan 2024 00:12:18 +0900 Subject: [PATCH 05/27] =?UTF-8?q?docs=20:=20Read=20Me=20ERD=20=EB=93=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 96b2bf6..0f9939f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# off-coupon -선착순 쿠폰 발행 서비스 입니다. (네고왕 이벤트 참고) +# 프로젝트 개요 off-coupon +image +선착순 쿠폰 발행 서비스 입니다. (네고왕 이벤트 참고 - 추후 설명 추가 예정) -## Git Flow +# Git Flow ✅ master : 릴리스 버전을 관리하는 메인 브랜치 ✅ develop : 개발이 진행되는 통합 브랜치 @@ -12,12 +13,23 @@ Reference : [우아한 형제들 기술블로그 : gitFlow](https://techblog.woowahan.com/2553/) -## Commit Convention +# 프로젝트 기술 스택 -✅ feat : 새로운 기능 추가 -✅ fix : 버그 수정 -✅ docs : 문서 수정 -✅ style : 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우 -✅ refactor : 코드 리팩토링 -✅ test : 테스트 코드, 리팩토링 테스트 코드 -✅ chore : 빌드 업무 수정, 패키지 매니저 수정 +- ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2.2-6DB33F?logo=spring%20boot&logoColor=6DB33F) +- ![MyBatis](https://img.shields.io/badge/MyBatis-3.0.2-000000?logo=&logoColor=000000) +- ![Gradle](https://img.shields.io/badge/Gradle-8.5-02303A?logo=gradle&logoColor=02303A) +- ![IntelliJ](https://img.shields.io/badge/IntelliJ-2023.1-000000?logo=intellijidea&logoColor=000000) + +# Commit Convention + +- feat : 새로운 기능 추가 +- fix : 버그 수정 +- docs : 문서 수정 +- style : 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우 +- refactor : 코드 리팩토링 +- test : 테스트 코드, 리팩토링 테스트 코드 +- chore : 빌드 업무 수정, 패키지 매니저 수정 + +# ERD + +image From 6c419e5c0eef5f6b2392dc19d31813ee9ab414c7 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 02:08:11 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat=20:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B0=96=EA=B3=A0=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=A1=B0=ED=9A=8C=20&=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=8B=9C=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=20=EC=BF=A0=ED=8F=B0=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/configs/SecurityConfig.java | 2 + .../controller/MyPageController.java | 29 +++++++ .../offcoupon/controller/OrderController.java | 35 +++++++++ .../mypage/AllCouponsByMemberIdVo.java | 23 ++++++ .../order/AvailableCouponsByMemberIdVo.java | 24 ++++++ .../order/MemberIdProductIdNowVo.java | 18 +++++ .../AllCouponsByMemberIdResponse.java | 31 ++++++++ .../AvailableCouponsByMemberIdResponse.java | 33 ++++++++ .../mysql/CouponIssueRepository.java | 33 ++++++++ .../async/consumer/CouponIssueConsumer.java | 5 +- .../service/coupon_use/OrderService.java | 27 +++++++ .../service/mypage/MyPageService.java | 24 ++++++ .../flab/offcoupon/util/DiscountUtils.java | 28 +++++++ src/main/resources/data.sql | 6 +- .../resources/mapper/CouponIssueMapper.xml | 63 +++++++++++++-- src/main/resources/schema.sql | 43 +++++++++- .../java/com/flab/offcoupon/QueryTest.java | 78 +++++++++++++++++++ 17 files changed, 489 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/flab/offcoupon/controller/MyPageController.java create mode 100644 src/main/java/com/flab/offcoupon/controller/OrderController.java create mode 100644 src/main/java/com/flab/offcoupon/domain/vo/persistence/mypage/AllCouponsByMemberIdVo.java create mode 100644 src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java create mode 100644 src/main/java/com/flab/offcoupon/domain/vo/persistence/order/MemberIdProductIdNowVo.java create mode 100644 src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java create mode 100644 src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java create mode 100644 src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java create mode 100644 src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java create mode 100644 src/main/java/com/flab/offcoupon/util/DiscountUtils.java create mode 100644 src/test/java/com/flab/offcoupon/QueryTest.java diff --git a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java index 9b70b6a..c095e2e 100644 --- a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java +++ b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java @@ -48,6 +48,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/v1/members/signup", "/main", "/api/v1/sse/**", + "/api/v1/purchase/**", + "/api/v1/orders/**", "/api/v1/event/**") .permitAll() .requestMatchers( diff --git a/src/main/java/com/flab/offcoupon/controller/MyPageController.java b/src/main/java/com/flab/offcoupon/controller/MyPageController.java new file mode 100644 index 0000000..335199d --- /dev/null +++ b/src/main/java/com/flab/offcoupon/controller/MyPageController.java @@ -0,0 +1,29 @@ +package com.flab.offcoupon.controller; + +import com.flab.offcoupon.dto.response.AllCouponsByMemberIdResponse; +import com.flab.offcoupon.service.mypage.MyPageService; +import com.flab.offcoupon.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/my-page") +@RestController +public class MyPageController { + + private final MyPageService myPageService; + @ResponseStatus(HttpStatus.OK) + @GetMapping("/coupons") + public ResponseEntity>> getAllCoupons(@RequestParam final long memberId) { + return ResponseEntity.status(HttpStatus.OK).body(myPageService.getAllCoupons(memberId)); + } + +// @GetMapping("/coupon-use") +// public void getAvailableCoupons(@RequestParam final long memberId) { +// couponUseService.getAvailableCoupons(memberId); +// } +} diff --git a/src/main/java/com/flab/offcoupon/controller/OrderController.java b/src/main/java/com/flab/offcoupon/controller/OrderController.java new file mode 100644 index 0000000..6121b56 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/controller/OrderController.java @@ -0,0 +1,35 @@ +package com.flab.offcoupon.controller; + +import com.flab.offcoupon.dto.response.AvailableCouponsByMemberIdResponse; +import com.flab.offcoupon.service.coupon_use.OrderService; +import com.flab.offcoupon.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/orders") +@RestController +public class OrderController { + + private final OrderService orderService; + + /** + * 사용 가능한 쿠폰 목록 조회 + * + * @param memberId 회원 ID + * @param productId 상품 ID + * @return 사용 가능한 쿠폰 목록 + */ + @ResponseStatus(HttpStatus.OK) + @GetMapping("/available-coupons") + public ResponseEntity>> getAvailableCoupons(@RequestParam final long memberId, + @RequestParam final long productId) { + LocalDateTime now = LocalDateTime.now(); + return ResponseEntity.status(HttpStatus.OK).body(orderService.getAvailableCoupons(memberId, productId, now)); + } +} diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/mypage/AllCouponsByMemberIdVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/mypage/AllCouponsByMemberIdVo.java new file mode 100644 index 0000000..e0a7b2f --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/vo/persistence/mypage/AllCouponsByMemberIdVo.java @@ -0,0 +1,23 @@ +package com.flab.offcoupon.domain.vo.persistence.mypage; + +import com.flab.offcoupon.domain.entity.CouponStatus; +import com.flab.offcoupon.domain.entity.DiscountType; + +import java.time.LocalDateTime; +/** + * MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO + * + * 마이페이지에서 현재 회원이 가진 모든 쿠폰을 조회하기 위해 사용 + */ +public record AllCouponsByMemberIdVo( + long couponId, + String category, + String description, + DiscountType discountType, + Long discountRate,// NULL 일 경우 AMOUNT + Long discountPrice,// NULL 일 경우 PERCENT + LocalDateTime validateStartDate, + LocalDateTime validateEndDate, + CouponStatus couponStatus + ) +{} diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java new file mode 100644 index 0000000..9862872 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java @@ -0,0 +1,24 @@ +package com.flab.offcoupon.domain.vo.persistence.order; + +import com.flab.offcoupon.domain.entity.CouponStatus; +import com.flab.offcoupon.domain.entity.DiscountType; + +import java.time.LocalDateTime; + +/** + * MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO + * 마이페이지에서 현재 회원이 가진 모든 쿠폰을 조회하기 위해 사용 + */ +public record AvailableCouponsByMemberIdVo ( + long couponId, + LocalDateTime validateStartDate, + LocalDateTime validateEndDate, + DiscountType discountType, + Long discountRate,// NULL 일 경우 AMOUNT + Long discountPrice,// NULL 일 경우 PERCENT + String category, + String description, + CouponStatus couponStatus, + long discountedPrice // 할인 가격 +) +{} diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/MemberIdProductIdNowVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/MemberIdProductIdNowVo.java new file mode 100644 index 0000000..5ce5bb8 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/MemberIdProductIdNowVo.java @@ -0,0 +1,18 @@ +package com.flab.offcoupon.domain.vo.persistence.order; + +import java.time.LocalDateTime; + +/** + * MyBatis에서 여러개의 파라미터를 전달하기 위한 VO + * + * @param memberId + * @param productId + * @param currentDateTime + */ +public record MemberIdProductIdNowVo( + long memberId, + long productId, + LocalDateTime currentDateTime +) { +} + diff --git a/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java new file mode 100644 index 0000000..e324e30 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java @@ -0,0 +1,31 @@ +package com.flab.offcoupon.dto.response; + +import com.flab.offcoupon.domain.entity.CouponStatus; +import com.flab.offcoupon.domain.vo.persistence.mypage.AllCouponsByMemberIdVo; +import com.flab.offcoupon.util.DiscountUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public final class AllCouponsByMemberIdResponse { + private final long couponId; + private final String category; + private final String description; + private final String discount; + private final LocalDateTime validateStartDate; + private final LocalDateTime validateEndDate; + private final CouponStatus couponStatus; + + public AllCouponsByMemberIdResponse(AllCouponsByMemberIdVo vo) { + this.couponId = vo.couponId(); + this.category = vo.category(); + this.description = vo.description(); + this.discount = DiscountUtils.getDiscount(vo.discountType(), vo.discountRate(), vo.discountPrice()); + this.validateStartDate = vo.validateStartDate(); + this.validateEndDate = vo.validateEndDate(); + this.couponStatus = vo.couponStatus(); + } +} diff --git a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java new file mode 100644 index 0000000..a117b15 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java @@ -0,0 +1,33 @@ +package com.flab.offcoupon.dto.response; + +import com.flab.offcoupon.domain.entity.CouponStatus; +import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; +import com.flab.offcoupon.util.DiscountUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public final class AvailableCouponsByMemberIdResponse { + private final long couponId; + private final String category; + private final String description; + private final String discount; + private final long discountPrice; + private final LocalDateTime validateStartDate; + private final LocalDateTime validateEndDate; + private final CouponStatus couponStatus; + + public AvailableCouponsByMemberIdResponse(AvailableCouponsByMemberIdVo vo) { + this.couponId = vo.couponId(); + this.category = vo.category(); + this.description = vo.description(); + this.discount = DiscountUtils.getDiscount(vo.discountType(), vo.discountRate(), vo.discountPrice()); + this.discountPrice = vo.discountedPrice(); + this.validateStartDate = vo.validateStartDate(); + this.validateEndDate = vo.validateEndDate(); + this.couponStatus = vo.couponStatus(); + } +} diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java index 04bbdd6..6b337c8 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java @@ -3,6 +3,9 @@ import com.flab.offcoupon.domain.entity.CouponIssue; import com.flab.offcoupon.domain.vo.persistence.couponissue.CountByCouponIdVo; import com.flab.offcoupon.domain.vo.persistence.couponissue.CouponIssueCheckVo; +import com.flab.offcoupon.domain.vo.persistence.mypage.AllCouponsByMemberIdVo; +import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; +import com.flab.offcoupon.domain.vo.persistence.order.MemberIdProductIdNowVo; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -12,9 +15,39 @@ @Mapper public interface CouponIssueRepository { void save(CouponIssue couponIssue); + boolean existCouponIssue(CouponIssueCheckVo couponIssueCheckVo); + List countCouponIdForToday(); + List couponIssueIdListForToday(); + Optional findCouponIssueById(long id); + void updateCheckFlag(CouponIssue couponIssue); + + /** + * 마이페이지에서 본인이 가진 모든 쿠폰 조회 + * + * @param memberId 회원 ID + * @return 쿠폰 목록 + */ + + List getAllCoupons(long memberId); + + /** + * 물건 구매 시 사용 가능한 쿠폰 조회
+ *
    + *
  • 사용 가능한 쿠폰 목록 조회 조건
  • + *
      + *
    1. 할인율이 적용된 가격(discounted_price) 기준으로 내림차순 정렬
    2. + *
    3. discounted_price는 쿠폰의 할인율 또는 할인액에 따라 원래 상품 가격 혹은 할인 상품 가격에 계산
    4. + *
    5. 쿠폰의 상태가 활성화인 상태이고, 현재 날짜 기준으로 유효 기간 범위 내에 있는 쿠폰 조회
    6. + *
    + *
+ * + * @param memberIdProductIdNowVo 회원 ID, 상품 ID, 현재 날짜 + * @return 사용 가능한 쿠폰 목록 + */ + List getAvailableCoupons(final MemberIdProductIdNowVo memberIdProductIdNowVo); } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java b/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java index bcbab37..c2bf5ed 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java @@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import static com.flab.offcoupon.util.CouponRabbitMQConstants.QUEUE_NAME; @@ -38,7 +37,7 @@ public class CouponIssueConsumer { /** * 3초마다 메시지 큐를 확인하여 메시지가 있는지 여부를 판단하고 쿠폰 이력을 INSERT합니다. */ - @Scheduled(fixedDelay = 3000) + // @Scheduled(fixedDelay = 3000) private void consumeCouponIssueMessage() { if (existCouponIssueQueueTarget()) { CouponIssueMessageForQueue message = (CouponIssueMessageForQueue) rabbitTemplate.receiveAndConvert(QUEUE_NAME); @@ -52,7 +51,7 @@ private void consumeCouponIssueMessage() { /** * 10초마다 오늘 발급된 쿠폰의 총 발급 수량을 조회해서 반정규화된 칼럼을 업데이트합니다. */ - @Scheduled(fixedDelay = 10000) + // @Scheduled(fixedDelay = 10000) private void updateTotalCouponIssueCount() { redissonLockHandler.asyncIssueCoupon(); } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java new file mode 100644 index 0000000..b2277a3 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -0,0 +1,27 @@ +package com.flab.offcoupon.service.coupon_use; + +import com.flab.offcoupon.domain.vo.persistence.order.MemberIdProductIdNowVo; +import com.flab.offcoupon.dto.response.AvailableCouponsByMemberIdResponse; +import com.flab.offcoupon.repository.mysql.CouponIssueRepository; +import com.flab.offcoupon.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class OrderService { + + private final CouponIssueRepository couponIssueRepository; + @Transactional(readOnly = true) + public ResponseDTO> getAvailableCoupons(final long memberId, final long productId, final LocalDateTime now) { + return ResponseDTO.getSuccessResult(couponIssueRepository.getAvailableCoupons(new MemberIdProductIdNowVo(memberId, productId, now)) + .stream() + .map(AvailableCouponsByMemberIdResponse::new) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java b/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java new file mode 100644 index 0000000..87a22c6 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java @@ -0,0 +1,24 @@ +package com.flab.offcoupon.service.mypage; + +import com.flab.offcoupon.dto.response.AllCouponsByMemberIdResponse; +import com.flab.offcoupon.repository.mysql.CouponIssueRepository; +import com.flab.offcoupon.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class MyPageService { + + private final CouponIssueRepository couponIssueRepository; + + public ResponseDTO> getAllCoupons(long memberId) { + return ResponseDTO.getSuccessResult(couponIssueRepository.getAllCoupons(memberId) + .stream() + .map(AllCouponsByMemberIdResponse::new) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java new file mode 100644 index 0000000..4a740b6 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java @@ -0,0 +1,28 @@ +package com.flab.offcoupon.util; + +import com.flab.offcoupon.domain.entity.DiscountType; +import lombok.experimental.UtilityClass; + +/** + * 할인 정보를 반환하는 유틸리티 클래스입니다. + */ +@UtilityClass +public class DiscountUtils { + /** + * 할인 정보를 반환하는 메소드입니다.
+ * + * String 더하기 연산은 성능이 좋지 않을 수 있으므로 StringBuilder를 사용하여 최적화하였습니다. + * @param discountType 할인 종류 (DiscountType enum 값) + * @param discountRate 할인율 (percent 할인의 경우) + * @param discountPrice 할인액 (amount 할인의 경우) + * @return 할인 정보를 나타내는 문자열 + */ + public static String getDiscount(DiscountType discountType, Long discountRate, Long discountPrice) { + StringBuilder sb = new StringBuilder(); + if (discountType == DiscountType.AMOUNT) { + return sb.append(discountPrice).append("원 할인").toString(); + } else { + return sb.append(discountRate).append("% 할인").toString(); + } + } +} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c4f9882..e9f5f5e 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -13,4 +13,8 @@ INSERT INTO event (id, category, description, start_date, end_date, daily_issue_ daily_issue_end_time, created_at, updated_at) VALUES (1, '바디케어' , '바디케어 전품목 이벤트', now(),DATE_ADD(now(), INTERVAL 3 DAY),'12:00:00','23:00:00','2024-02-01','2024-02-01'); - +## PRODUCT +INSERT INTO product (category, title, description, original_price, sale_price, created_at, updated_at) +VALUES + ('의류', '면티셔츠', '편안한 면소재의 티셔츠', 20000, NULL, NOW(), NOW()), + ('전자제품', '스마트폰', '고성능 스마트폰', 1000000, 800000, NOW(), NOW()); diff --git a/src/main/resources/mapper/CouponIssueMapper.xml b/src/main/resources/mapper/CouponIssueMapper.xml index 2e7dfa1..6dc0bc1 100644 --- a/src/main/resources/mapper/CouponIssueMapper.xml +++ b/src/main/resources/mapper/CouponIssueMapper.xml @@ -6,7 +6,8 @@ - INSERT INTO coupon_issue (member_id, coupon_id, coupon_status, created_at, updated_at, check_related_issued_quantity) + INSERT INTO coupon_issue (member_id, coupon_id, coupon_status, created_at, updated_at, + check_related_issued_quantity) VALUES (#{memberId}, #{couponId}, #{couponStatus}, #{createdAt}, #{updatedAt}, #{checkRelatedIssuedQuantity}); @@ -15,21 +16,23 @@ FROM coupon_issue WHERE member_id = #{memberId} AND coupon_id = #{couponId} - AND DATE (created_at) = #{currentDateTime} - LIMIT 1); + AND DATE(created_at) = #{currentDateTime} + LIMIT 1); + SELECT c.id, + e.category, + e.description, + c.discount_type, + c.discount_rate, + c.discount_price, + c.validate_start_date, + c.validate_end_date, + ci.coupon_status + FROM coupon_issue ci + JOIN coupon c on ci.coupon_id = c.id + JOIN event e on c.event_id = e.id + WHERE ci.member_id = #{memberId}; + + + \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 272e7ca..cd4da42 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -2,6 +2,7 @@ DROP TABLE IF EXISTS member; DROP TABLE IF EXISTS coupon; DROP TABLE IF EXISTS event; DROP TABLE IF EXISTS coupon_issue; +DROP TABLE IF EXISTS product; CREATE TABLE member ( @@ -19,7 +20,7 @@ CREATE TABLE member CREATE TABLE event ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '이벤트 식별자', - category VARCHAR(100) NOT NULL COMMENT '이벤트 카테고리', + category VARCHAR(100) NOT NULL COMMENT '상품 카테고리', description VARCHAR(255) NOT NULL COMMENT '이벤트 설명', start_date DATE NULL COMMENT '이벤트 시작일 / null일 경우 무제한 이벤트', end_date DATE NULL COMMENT '이벤트 종료일 / null일 경우 무제한 이벤트', @@ -44,15 +45,49 @@ CREATE TABLE coupon created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); - -CREATE TABLE coupon_issue +# 쿼리 최적화 성능 비교 테스트용 +CREATE TABLE coupon_issue_no_idx ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰 발행 기록', member_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', coupon_id BIGINT UNSIGNED NOT NULL COMMENT '회원 ID', - coupon_status VARCHAR(50) NOT NULL DEFAULT 'NOT_ACTIVE' COMMENT '유효일 전 : NOT_ACTIVE / 유효기간 : ACTIVE / 사용완료 : USED / 만료 : EXPIRED', + coupon_status VARCHAR(255) NOT NULL DEFAULT 'NOT_ACTIVE' COMMENT '유효일 전 : NOT_ACTIVE / 유효기간 : ACTIVE / 사용완료 : USED / 만료 : EXPIRED', created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일', check_related_issued_quantity BOOLEAN DEFAULT FALSE COMMENT '쿠폰 발행시 발행량 체크 여부' ); +CREATE TABLE product +( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '상품 식별자', + category VARCHAR(100) NOT NULL COMMENT '상품 카테고리', + title VARCHAR(100) NOT NULL COMMENT '상품명', + description VARCHAR(255) NOT NULL COMMENT '상품 설명', + original_price BIGINT UNSIGNED NOT NULL COMMENT '원래 상품 가격', + sale_price BIGINT UNSIGNED NULL COMMENT '할인 상품 가격(쿠폰과 관계없이 전체적으로 할인할 경우)', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' +); + + +CREATE TABLE order_detail +( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '주문 식별자', + product_id BIGINT UNSIGNED NOT NULL COMMENT '상품 ID', + quantity BIGINT UNSIGNED NOT NULL COMMENT '상품 주문 수량', + price_per_each BIGINT UNSIGNED NOT NULL COMMENT '상품 개당 가격', + total_order_price BIGINT UNSIGNED NOT NULL COMMENT '총 상품 주문 가격', + discount_price BIGINT UNSIGNED NOT NULL COMMENT '할인 가격', + total_payment_price BIGINT UNSIGNED NOT NULL COMMENT '총 결제 가격', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' +); + +CREATE TABLE order_coupon ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '주문 쿠폰 식별자', + order_id BIGINT UNSIGNED NOT NULL COMMENT '주문 ID', + coupon_id VARCHAR(50) NOT NULL COMMENT '쿠폰 ID', + discount_amount BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 할인액', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' +); diff --git a/src/test/java/com/flab/offcoupon/QueryTest.java b/src/test/java/com/flab/offcoupon/QueryTest.java new file mode 100644 index 0000000..64de57d --- /dev/null +++ b/src/test/java/com/flab/offcoupon/QueryTest.java @@ -0,0 +1,78 @@ +package com.flab.offcoupon; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.*; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QueryTest { + + private static final String url = "jdbc:mysql://localhost/off_coupon"; + private static final String user = "root"; + private static final String password = "1234"; + private static final int queryCount = 10; + private static long totalExecutionTimeWithIndex = 0; + private static long totalExecutionTimeWithoutIndex = 0; + private static Connection connection; + + @BeforeAll + static void setUpBeforeClass() throws Exception { + connection = DriverManager.getConnection(url, user, password); + } + + @AfterAll + static void tearDownAfterClass() throws Exception { + if (connection != null) { + connection.close(); + } + } + + @Test + @DisplayName("[쿼리 실행 시간 측정] 인덱스 있는 쿼리 실행 시간 비교") + public void testQueryWithIndex() throws SQLException { + for (int i = 0; i < queryCount; i++) { + long startTime = System.currentTimeMillis(); // 시작 시간 기록 + try (Statement statement = connection.createStatement()) { + String sql = "SELECT e.category, e.description, c.discount_type, c.discount_rate, c.discount_price, c.validate_start_date, c.validate_end_date, ci.coupon_status FROM coupon_issue ci JOIN coupon c on ci.coupon_id = c.id JOIN event e on c.event_id = e.id WHERE ci.member_id = 1;"; + ResultSet resultSet = statement.executeQuery(sql); + // 쿼리 실행 결과 사용하지 않음 (예제에서는 단순 실행 시간 측정 목적) + } + long endTime = System.currentTimeMillis(); // 종료 시간 기록 + long executionTime = endTime - startTime; // 실행 시간 계산 + totalExecutionTimeWithIndex += executionTime; // 총 실행 시간 누적 + System.out.println("Query with index " + (i + 1) + ": Execution time " + executionTime + " milliseconds"); + } + } + + @Test + @DisplayName("[쿼리 실행 시간 측정] 인덱스 없는 쿼리 실행 시간 비교") + public void testQueryWithoutIndex() throws SQLException { + for (int i = 0; i < queryCount; i++) { + long startTime = System.currentTimeMillis(); // 시작 시간 기록 + try (Statement statement = connection.createStatement()) { + String sql = "SELECT e.category, e.description, c.discount_type, c.discount_rate, c.discount_price, c.validate_start_date, c.validate_end_date, ci.coupon_status FROM coupon_issue_no_idx ci JOIN coupon c on ci.coupon_id = c.id JOIN event e on c.event_id = e.id WHERE ci.member_id = 1;"; + ResultSet resultSet = statement.executeQuery(sql); + // 쿼리 실행 결과 사용하지 않음 (예제에서는 단순 실행 시간 측정 목적) + } + long endTime = System.currentTimeMillis(); // 종료 시간 기록 + long executionTime = endTime - startTime; // 실행 시간 계산 + totalExecutionTimeWithoutIndex += executionTime; // 총 실행 시간 누적 + System.out.println("Query without index " + (i + 1) + ": Execution time " + executionTime + " milliseconds"); + } + } + + @Test + @DisplayName("[평균 실행 시간 계산] 인덱스 생성 전후 쿼리 실행 시간 비교") + public void testAverageExecutionTime() { + // 평균 실행 시간 계산 + double averageExecutionTimeWithIndex = (double) totalExecutionTimeWithIndex / queryCount; + double averageExecutionTimeWithoutIndex = (double) totalExecutionTimeWithoutIndex / queryCount; + System.out.println("Average execution time with index: " + averageExecutionTimeWithIndex + " milliseconds"); + System.out.println("Average execution time without index: " + averageExecutionTimeWithoutIndex + " milliseconds"); + assertTrue(averageExecutionTimeWithIndex < averageExecutionTimeWithoutIndex); + } +} \ No newline at end of file From d85e5a99143a717490c7e7111ce4a8600312ee75 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 18:30:33 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat=20:=20=EC=BF=A0=ED=8F=B0=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offcoupon/domain/entity/CouponStatus.java | 2 +- .../offcoupon/domain/entity/OrderCoupon.java | 38 +++++++++++ .../offcoupon/domain/entity/OrderDetail.java | 44 +++++++++++++ .../flab/offcoupon/domain/entity/Product.java | 3 +- .../entity/params/AppliedCouponInfo.java | 24 +++++++ .../domain/entity/params/OrderInfo.java | 41 ++++++++++++ .../order/AvailableCouponsByMemberIdVo.java | 1 + .../AvailableCouponsByMemberIdResponse.java | 2 + .../mysql/CouponIssueRepository.java | 6 ++ .../repository/mysql/CouponRepository.java | 35 +++++++++- .../mysql/OrderCouponRepository.java | 9 +++ .../mysql/OrderDetailRepository.java | 9 +++ .../repository/mysql/ProductRepository.java | 7 ++ .../service/cache/CouponCacheService.java | 5 +- .../consumer/CouponIssueMessageHandler.java | 5 +- .../sync/DefaultCouponIssueService.java | 5 +- .../service/coupon_use/OrderService.java | 61 +++++++++++++---- .../flab/offcoupon/util/DiscountUtils.java | 66 +++++++++++++++++-- .../resources/mapper/CouponIssueMapper.xml | 14 +++- src/main/resources/mapper/CouponMapper.xml | 22 +++++++ .../resources/mapper/OrderCouponMapper.xml | 12 ++++ .../resources/mapper/OrderDetailMapper.xml | 13 ++++ src/main/resources/mapper/ProductMapper.xml | 6 ++ src/main/resources/schema.sql | 4 +- .../AsyncDefaultCouponIssueServiceTest.java | 6 +- ...LettuceLockCouponIssueConcurrencyTest.java | 2 +- .../NamedLockCouponIssueConcurrencyTest.java | 2 +- ...imisticLockCouponIssueConcurrencyTest.java | 2 +- ...edissonLockCouponIssueConcurrencyTest.java | 2 +- 29 files changed, 403 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java create mode 100644 src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java create mode 100644 src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java create mode 100644 src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java create mode 100644 src/main/java/com/flab/offcoupon/repository/mysql/OrderCouponRepository.java create mode 100644 src/main/java/com/flab/offcoupon/repository/mysql/OrderDetailRepository.java create mode 100644 src/main/resources/mapper/OrderCouponMapper.xml create mode 100644 src/main/resources/mapper/OrderDetailMapper.xml diff --git a/src/main/java/com/flab/offcoupon/domain/entity/CouponStatus.java b/src/main/java/com/flab/offcoupon/domain/entity/CouponStatus.java index 4df4dd2..cd6379b 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/CouponStatus.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/CouponStatus.java @@ -4,7 +4,7 @@ public enum CouponStatus { NOT_ACTIVE("유효기간 시작 전"), ACTIVE("유효 기간 중"), USED("사용 완료"), - EXPIRED("기간 완료"); + EXPIRED("기간 만료"); private final String description; diff --git a/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java b/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java new file mode 100644 index 0000000..d6c8f17 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java @@ -0,0 +1,38 @@ +package com.flab.offcoupon.domain.entity; + +import com.flab.offcoupon.domain.entity.params.AppliedCouponInfo; +import com.flab.offcoupon.domain.entity.params.TimeParams; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * 주문에 사용된 쿠폰 정보를 담는 도메인 객체입니다. + * 하나의 주문에 여러 쿠폰을 사용할 수 있기 때문에 별도의 테이블로 분리하였습니다. + */ +@ToString +@Getter +@AllArgsConstructor +public final class OrderCoupon { + private long id; + private final long orderDetailId; + private final long couponId; + private final long discountAmount; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + private OrderCoupon(long orderDetailId, AppliedCouponInfo coupon, TimeParams timeParams){ + this.orderDetailId = orderDetailId; + this.couponId = coupon.getCouponId(); + this.discountAmount = coupon.getDiscountAmount(); + this.createdAt = timeParams.createdAt(); + this.updatedAt = timeParams.updatedAt(); + } + + public static OrderCoupon createOrderCoupon(long orderDetailId, AppliedCouponInfo coupon) { + LocalDateTime now = LocalDateTime.now(); + return new OrderCoupon(orderDetailId, coupon, new TimeParams(now, now)); + } +} \ No newline at end of file diff --git a/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java b/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java new file mode 100644 index 0000000..0ab30d7 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java @@ -0,0 +1,44 @@ +package com.flab.offcoupon.domain.entity; + +import com.flab.offcoupon.domain.entity.params.OrderInfo; +import com.flab.offcoupon.domain.entity.params.TimeParams; +import com.flab.offcoupon.util.DateTimeUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * 주문 상세 정보를 담는 도메인 객체입니다. + */ +@ToString +@Getter +@AllArgsConstructor +public final class OrderDetail { + private long id; + private final long productId; + private final long quantity; + private final long pricePerEach; // 상품 1개당 가격 + private final long totalOrderPrice; // 총 상품 주문 가격 + private final long totalDiscountPrice; // 할인 가격 + private final long totalPaymentPrice; // 총 상품 주문 가격 - 할인 가격 + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + private OrderDetail(OrderInfo orderInfo, TimeParams timeParams) { + this.productId = orderInfo.getProductId(); + this.quantity = orderInfo.getQuantity(); + this.pricePerEach = orderInfo.getPricePerEach(); + this.totalOrderPrice = orderInfo.getTotalOrderPrice(); + this.totalDiscountPrice = orderInfo.getTotalDiscountPrice(); + this.totalPaymentPrice = orderInfo.getTotalPaymentPrice(); + this.createdAt = timeParams.createdAt(); + this.updatedAt = timeParams.updatedAt(); + } + + public static OrderDetail createOrderDetail(OrderInfo orderInfo) { + LocalDateTime now = DateTimeUtils.nowFromZone(); + return new OrderDetail(orderInfo, new TimeParams(now, now)); + } +} diff --git a/src/main/java/com/flab/offcoupon/domain/entity/Product.java b/src/main/java/com/flab/offcoupon/domain/entity/Product.java index d305de0..098b8cb 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/Product.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/Product.java @@ -18,8 +18,9 @@ public final class Product { private final String category; private final String title; private final String description; - private final BigDecimal originalPrice; + private final BigDecimal originalPrice; // 원래 가격 private final BigDecimal salePrice; // null일 경우 전체 할인이 적용되지 않은 것으로 간주 + private final BigDecimal minOrderPrice; // 최소 주문 가격 private final LocalDateTime createdAt; private final LocalDateTime updatedAt; } diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java new file mode 100644 index 0000000..66cb7e4 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java @@ -0,0 +1,24 @@ +package com.flab.offcoupon.domain.entity.params; + +import com.flab.offcoupon.domain.entity.Coupon; +import com.flab.offcoupon.domain.entity.Product; +import com.flab.offcoupon.util.DiscountUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public final class AppliedCouponInfo { + + private final long couponId; + private final long discountAmount; + + private AppliedCouponInfo(Product product, Coupon coupon) { + this.couponId = coupon.getId(); + this.discountAmount = DiscountUtils.calculateEachDiscountPrice(product, coupon); + } + + public static AppliedCouponInfo createAppliedCouponInfo(Product product, Coupon coupon) { + return new AppliedCouponInfo(product, coupon); + } +} diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java new file mode 100644 index 0000000..64981b7 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java @@ -0,0 +1,41 @@ +package com.flab.offcoupon.domain.entity.params; + +import com.flab.offcoupon.domain.entity.Coupon; +import com.flab.offcoupon.domain.entity.Product; +import com.flab.offcoupon.util.DiscountUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public final class OrderInfo { + private final long productId; + private final long quantity; + private final long pricePerEach; + private final long totalOrderPrice; + private final List appliedCouponInfos; + private final long totalDiscountPrice; + private final long totalPaymentPrice; + + private OrderInfo(Product product, long quantity, List couponList) { + this.productId = product.getId(); + this.quantity = quantity; + this.pricePerEach = DiscountUtils.pricePerEach(product); + this.totalOrderPrice = DiscountUtils.totalOrderPrice(product, quantity); + this.appliedCouponInfos = createAppliedCouponInfos(product, couponList); + this.totalDiscountPrice = DiscountUtils.totalDiscountPrice(product, couponList, quantity); + this.totalPaymentPrice = DiscountUtils.totalPaymentPrice(product, quantity, couponList); + } + + public static OrderInfo createOrderInfo(Product product, long quantity, List couponList) { + return new OrderInfo(product, quantity, couponList); + } + + private List createAppliedCouponInfos(Product product, List couponList) { + return couponList.stream() + .map(coupon -> AppliedCouponInfo.createAppliedCouponInfo(product, coupon)) + .toList(); + } +} diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java index 9862872..e63151f 100644 --- a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java +++ b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java @@ -18,6 +18,7 @@ public record AvailableCouponsByMemberIdVo ( Long discountPrice,// NULL 일 경우 PERCENT String category, String description, + long couponIssueId, CouponStatus couponStatus, long discountedPrice // 할인 가격 ) diff --git a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java index a117b15..6f175da 100644 --- a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java +++ b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java @@ -18,6 +18,7 @@ public final class AvailableCouponsByMemberIdResponse { private final long discountPrice; private final LocalDateTime validateStartDate; private final LocalDateTime validateEndDate; + private final long couponIssueId; private final CouponStatus couponStatus; public AvailableCouponsByMemberIdResponse(AvailableCouponsByMemberIdVo vo) { @@ -28,6 +29,7 @@ public AvailableCouponsByMemberIdResponse(AvailableCouponsByMemberIdVo vo) { this.discountPrice = vo.discountedPrice(); this.validateStartDate = vo.validateStartDate(); this.validateEndDate = vo.validateEndDate(); + this.couponIssueId = vo.couponIssueId(); this.couponStatus = vo.couponStatus(); } } diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java index 17b9948..e6f8488 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java @@ -60,4 +60,10 @@ public interface CouponIssueRepository { */ List validateStatusIsActive(List couponIssueIds); + + /** + * 쿠폰 상태를 사용 완료로 변경 + * @param couponIssueIds 쿠폰 발급 ID 목록 + */ + void updateCouponStatus(List couponIssueIds); } diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java index 3fe1421..f5846ae 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java @@ -3,6 +3,7 @@ import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.vo.persistence.couponissue.UpdateTotalIssuedQuantityVo; import com.flab.offcoupon.domain.vo.persistence.order.ValidateNowIsBetweenPeriodVo; +import com.flab.offcoupon.exception.coupon.CouponNotFoundException; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -10,12 +11,42 @@ import java.util.List; import java.util.Optional; +import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_NOT_EXIST; + @Mapper public interface CouponRepository { void save(Coupon coupon); + /** + * 쿠폰 ID로 쿠폰을 조회합니다.
+ * Coupon 객체를 찾을 수도 있고, 없을 수도 있습니다. + * Optional을 return하여 선택권을 제공합니다. + * + * @param couponId + * @return Optinal한 쿠폰 객체 + */ Optional findCouponById(long couponId); + /** + * 쿠폰 ID로 쿠폰을 조회해서 Coupon객체를 반환합니다.
+ * + * @param couponId 쿠폰 ID + * @return 쿠폰 객체 + */ + default Coupon getCouponById(long couponId) { + return findCouponById(couponId) + .orElseThrow(() -> new CouponNotFoundException(COUPON_NOT_EXIST.formatted(couponId))); + } + + /** + * 쿠폰 ID로 쿠폰 리스트를을조회합니다.
+ * + * @param couponIds 쿠폰 ID 리스트 + * @return List 쿠폰 객체 리스트 + */ + List findCouponsByIds(List couponIds); + + Optional findCouponByIdPessimisticLock(long couponId); void increaseIssuedQuantity(Coupon coupon); @@ -31,8 +62,8 @@ public interface CouponRepository { /** * 현재 시간이 쿠폰의 유효기간 범위내에 있는지 확인 * - * @param couponIds 쿠폰 ID 리스트 - * @param currentDateTime 현재 시간 + * @param couponIds 쿠폰 ID 리스트 + * @param currentDateTime 현재 시간 * @return 현재 시간이 쿠폰의 유효기간 범위내에 있는지 여부 */ List validateNowIsBetweenPeriod(@Param("couponIds") List couponIds, diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/OrderCouponRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/OrderCouponRepository.java new file mode 100644 index 0000000..d66cd30 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/repository/mysql/OrderCouponRepository.java @@ -0,0 +1,9 @@ +package com.flab.offcoupon.repository.mysql; + +import com.flab.offcoupon.domain.entity.OrderCoupon; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OrderCouponRepository { + void save(OrderCoupon orderCoupon); +} diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/OrderDetailRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/OrderDetailRepository.java new file mode 100644 index 0000000..183a71b --- /dev/null +++ b/src/main/java/com/flab/offcoupon/repository/mysql/OrderDetailRepository.java @@ -0,0 +1,9 @@ +package com.flab.offcoupon.repository.mysql; + +import com.flab.offcoupon.domain.entity.OrderDetail; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OrderDetailRepository { + Long save(OrderDetail orderDetail); +} diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java index 21701e6..1140e50 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java @@ -26,4 +26,11 @@ default Product getProductById(long productId) { return findProductById(productId) .orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다. id: " + productId)); } + + /** + * 상품의 최소 주문 가격을 조회합니다. + * @param productId 상품 ID + * @return 최소 주문 가격 + */ + long getProductMinOrderPriceById(long productId); } diff --git a/src/main/java/com/flab/offcoupon/service/cache/CouponCacheService.java b/src/main/java/com/flab/offcoupon/service/cache/CouponCacheService.java index e20e44d..1388ee3 100644 --- a/src/main/java/com/flab/offcoupon/service/cache/CouponCacheService.java +++ b/src/main/java/com/flab/offcoupon/service/cache/CouponCacheService.java @@ -8,8 +8,6 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_NOT_EXIST; - /** * 쿠폰 캐시 서비스를 제공하는 클래스입니다. */ @@ -40,7 +38,6 @@ public CouponRedisEntity getCoupon(long couponId) { * @throws CouponNotFoundException 쿠폰이 존재하지 않을 경우 발생하는 예외 */ private Coupon findCoupon(long couponId) { - return couponRepository.findCouponById(couponId) - .orElseThrow(() -> new CouponNotFoundException(COUPON_NOT_EXIST.formatted(couponId))); + return couponRepository.getCouponById(couponId); } } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueMessageHandler.java b/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueMessageHandler.java index a104887..365f768 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueMessageHandler.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueMessageHandler.java @@ -6,7 +6,6 @@ import com.flab.offcoupon.domain.vo.persistence.couponissue.UpdateTotalIssuedQuantityVo; import com.flab.offcoupon.dto.request.rabbit_mq.CouponIssueMessageForQueue; import com.flab.offcoupon.exception.coupon.CouponIssueException; -import com.flab.offcoupon.exception.coupon.CouponNotFoundException; import com.flab.offcoupon.repository.mysql.CouponIssueRepository; import com.flab.offcoupon.repository.mysql.CouponRepository; import com.flab.offcoupon.repository.redis.RedisRepository; @@ -18,7 +17,6 @@ import java.util.List; import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_ISSUE_NOT_EXIST; -import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_NOT_EXIST; import static com.flab.offcoupon.util.RedisKeyUtils.getIssueRequestKey; @Slf4j @@ -73,8 +71,7 @@ private void totalUpdateIssuedCouponAndDeleteRequest(List cou * 총 발행된 쿠폰 수량을 업데이트합니다. */ private void totalUpdateIssuedQuantity(CountByCouponIdVo countByCouponIdVo) { - Coupon coupon = couponRepository.findCouponById(countByCouponIdVo.couponId()) - .orElseThrow(() -> new CouponNotFoundException(COUPON_NOT_EXIST)); + Coupon coupon = couponRepository.getCouponById(countByCouponIdVo.couponId()); couponRepository.updateTotalIssuedCouponQuantity( new UpdateTotalIssuedQuantityVo(countByCouponIdVo.couponId(), coupon.getIssuedQuantity() + countByCouponIdVo.count())); diff --git a/src/main/java/com/flab/offcoupon/service/coupon_issue/sync/DefaultCouponIssueService.java b/src/main/java/com/flab/offcoupon/service/coupon_issue/sync/DefaultCouponIssueService.java index f7e805f..396375c 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_issue/sync/DefaultCouponIssueService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_issue/sync/DefaultCouponIssueService.java @@ -4,7 +4,6 @@ import com.flab.offcoupon.domain.entity.CouponIssue; import com.flab.offcoupon.domain.redis.EventRedisEntity; import com.flab.offcoupon.domain.vo.persistence.couponissue.CouponIssueCheckVo; -import com.flab.offcoupon.exception.coupon.CouponNotFoundException; import com.flab.offcoupon.exception.coupon.DuplicatedCouponException; import com.flab.offcoupon.repository.mysql.CouponIssueRepository; import com.flab.offcoupon.repository.mysql.CouponRepository; @@ -18,7 +17,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_NOT_EXIST; import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.DUPLICATED_COUPON; /** @@ -70,7 +68,6 @@ private void checkAlreadyIssueHistory(long memberId, long couponId, LocalDate cu } } private Coupon findCoupon(long couponId) { - return couponRepository.findCouponById(couponId) - .orElseThrow(() -> new CouponNotFoundException(COUPON_NOT_EXIST.formatted(couponId))); + return couponRepository.getCouponById(couponId); } } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index a6567bc..862ef10 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -1,22 +1,27 @@ package com.flab.offcoupon.service.coupon_use; -import com.flab.offcoupon.domain.entity.Product; +import com.flab.offcoupon.domain.entity.OrderDetail; +import com.flab.offcoupon.domain.entity.params.AppliedCouponInfo; +import com.flab.offcoupon.domain.entity.params.OrderInfo; +import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; import com.flab.offcoupon.domain.vo.persistence.order.CouponIssuesAreActiveVo; import com.flab.offcoupon.domain.vo.persistence.order.MemberIdProductIdNowVo; import com.flab.offcoupon.domain.vo.persistence.order.ValidateNowIsBetweenPeriodVo; import com.flab.offcoupon.dto.request.OrderProductRequest; import com.flab.offcoupon.dto.response.AvailableCouponsByMemberIdResponse; -import com.flab.offcoupon.repository.mysql.CouponIssueRepository; -import com.flab.offcoupon.repository.mysql.CouponRepository; -import com.flab.offcoupon.repository.mysql.ProductRepository; +import com.flab.offcoupon.repository.mysql.*; import com.flab.offcoupon.util.ResponseDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; + +import static com.flab.offcoupon.domain.entity.OrderCoupon.createOrderCoupon; +import static com.flab.offcoupon.domain.entity.OrderDetail.createOrderDetail; +import static com.flab.offcoupon.domain.entity.params.OrderInfo.createOrderInfo; @RequiredArgsConstructor @Service @@ -25,26 +30,54 @@ public class OrderService { private final CouponIssueRepository couponIssueRepository; private final CouponRepository couponRepository; private final ProductRepository productRepository; + private final OrderDetailRepository orderDetailRepository; + private final OrderCouponRepository orderCouponRepository; + @Transactional(readOnly = true) - public ResponseDTO> getAvailableCoupons(final long memberId, final long productId, final LocalDateTime now) { - return ResponseDTO.getSuccessResult(couponIssueRepository.getAvailableCoupons(new MemberIdProductIdNowVo(memberId, productId, now)) - .stream() - .map(AvailableCouponsByMemberIdResponse::new) - .collect(Collectors.toList())); + public ResponseDTO> getAvailableCoupons(final long memberId, final long productId, final LocalDateTime now) { + List availableCoupons = + couponIssueRepository.getAvailableCoupons(new MemberIdProductIdNowVo(memberId, productId, now)); + List responseList = new ArrayList<>(); + long totalOrderPrice = 0; + // min_price(상품의 최소 주문 금액)과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. // TODO : NULL이 아니라 + for (AvailableCouponsByMemberIdVo availableCoupon : availableCoupons) { + // 최소 주문 금액까지 할인 가능한 금액 누적 + totalOrderPrice += availableCoupon.discountedPrice(); + // 할인 금액이 min_price보다 작거나 같으면 결과 리스트에 추가 + if (totalOrderPrice <= productRepository.getProductMinOrderPriceById(productId)) { + AvailableCouponsByMemberIdResponse response = new AvailableCouponsByMemberIdResponse(availableCoupon); + responseList.add(response); + } + } + return ResponseDTO.getSuccessResult(responseList); } + @Transactional public void orderProduct(final long productId, final OrderProductRequest request, LocalDateTime now) { validateCouponIsAvailable(request, now); - // 3. 주문 - 쿠폰 사용 처리(상태변경), 주문 정보 저장, 주문에 사용된 쿠폰 저장 - Product product = productRepository.getProductById(productId); - System.out.println("product : " + product); + // 3. 주문 정보 저장, 주문에 사용된 쿠폰 저장 + OrderInfo orderInfo = createOrderInfo( + productRepository.getProductById(productId), + request.getQuantity(), + couponRepository.findCouponsByIds(request.getCouponId())); + OrderDetail orderDetail = createOrderDetail(orderInfo); + orderDetailRepository.save(orderDetail); + long orderDetailId = orderDetail.getId(); + + // 4. 주문에 사용된 쿠폰 저장 + for (AppliedCouponInfo couponInfo : orderInfo.getAppliedCouponInfos()) { + orderCouponRepository.save(createOrderCoupon(orderDetailId, couponInfo)); + } + // 5. 쿠폰 사용 처리 + couponIssueRepository.updateCouponStatus(request.getCouponIssueId()); } /** * 쿠폰 사용 가능 여부 검증 + * * @param request 주문 요청 정보 - * @param now 현재 시간 + * @param now 현재 시간 */ private void validateCouponIsAvailable(OrderProductRequest request, LocalDateTime now) { // 1. 쿠폰들의 상태가 ACTIVE인지 확인 diff --git a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java index 4a740b6..8d589c0 100644 --- a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java +++ b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java @@ -1,8 +1,12 @@ package com.flab.offcoupon.util; +import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.entity.DiscountType; +import com.flab.offcoupon.domain.entity.Product; import lombok.experimental.UtilityClass; +import java.util.List; + /** * 할인 정보를 반환하는 유틸리티 클래스입니다. */ @@ -10,14 +14,15 @@ public class DiscountUtils { /** * 할인 정보를 반환하는 메소드입니다.
- * + *

* String 더하기 연산은 성능이 좋지 않을 수 있으므로 StringBuilder를 사용하여 최적화하였습니다. - * @param discountType 할인 종류 (DiscountType enum 값) - * @param discountRate 할인율 (percent 할인의 경우) + * + * @param discountType 할인 종류 (DiscountType enum 값) + * @param discountRate 할인율 (percent 할인의 경우) * @param discountPrice 할인액 (amount 할인의 경우) * @return 할인 정보를 나타내는 문자열 */ - public static String getDiscount(DiscountType discountType, Long discountRate, Long discountPrice) { + public String getDiscount(DiscountType discountType, Long discountRate, Long discountPrice) { StringBuilder sb = new StringBuilder(); if (discountType == DiscountType.AMOUNT) { return sb.append(discountPrice).append("원 할인").toString(); @@ -25,4 +30,57 @@ public static String getDiscount(DiscountType discountType, Long discountRate, L return sb.append(discountRate).append("% 할인").toString(); } } + + public long pricePerEach(Product product) { + return product.getSalePrice() == null ? product.getOriginalPrice().longValue() : product.getSalePrice().longValue(); + } + + public long totalOrderPrice(Product product, long quantity) { + return pricePerEach(product) * quantity; + } + + public long totalDiscountPrice(Product product, List couponList, long quantity) { + long totalDiscountPrice = 0; + for (Coupon coupon : couponList) { + if (coupon.getDiscountType() == DiscountType.PERCENT) { + double test = pricePerEach(product) * ((double)coupon.getDiscountRate() / 100); + System.out.println("pricePerEach: " + pricePerEach(product)); + System.out.println("coupon.getDiscountRate() : " + coupon.getDiscountRate()); + System.out.println("(coupon.getDiscountRate() / 100 : " + (coupon.getDiscountRate() / 100)); + System.out.println("test: " + test); + totalDiscountPrice += test; + } else { + totalDiscountPrice += coupon.getDiscountPrice(); + } + } + return totalDiscountPrice * quantity; + } + + public long calculateEachDiscountPrice(Product product, List couponList) { + long totalDiscountPrice = 0; + for (Coupon coupon : couponList) { + if (coupon.getDiscountType() == DiscountType.PERCENT) { + totalDiscountPrice += pricePerEach(product) * (coupon.getDiscountRate() / 100); + } else { + totalDiscountPrice += coupon.getDiscountPrice(); + } + } + return totalDiscountPrice; + } + + public long calculateEachDiscountPrice(Product product, Coupon coupon) { + long totalDiscountPrice = 0; + + if (coupon.getDiscountType() == DiscountType.PERCENT) { + totalDiscountPrice += pricePerEach(product) * coupon.getDiscountRate() / 100; + } else { + totalDiscountPrice += coupon.getDiscountPrice(); + } + + return totalDiscountPrice; + } + + public long totalPaymentPrice(Product product, long quantity, List couponList) { + return totalOrderPrice(product, quantity) - totalDiscountPrice(product, couponList, quantity); + } } diff --git a/src/main/resources/mapper/CouponIssueMapper.xml b/src/main/resources/mapper/CouponIssueMapper.xml index 050cedd..702d905 100644 --- a/src/main/resources/mapper/CouponIssueMapper.xml +++ b/src/main/resources/mapper/CouponIssueMapper.xml @@ -71,7 +71,7 @@ - + + UPDATE coupon_issue + SET coupon_status = 'USED' + WHERE id in + ( + + #{item} + + ) + \ No newline at end of file diff --git a/src/main/resources/mapper/CouponMapper.xml b/src/main/resources/mapper/CouponMapper.xml index 3ab6538..9ce97e6 100644 --- a/src/main/resources/mapper/CouponMapper.xml +++ b/src/main/resources/mapper/CouponMapper.xml @@ -29,6 +29,28 @@ WHERE id = #{id}; + + UPDATE coupon SET issued_quantity = #{issuedQuantity} diff --git a/src/main/resources/mapper/OrderCouponMapper.xml b/src/main/resources/mapper/OrderCouponMapper.xml new file mode 100644 index 0000000..84ba3a7 --- /dev/null +++ b/src/main/resources/mapper/OrderCouponMapper.xml @@ -0,0 +1,12 @@ + + + + + + + INSERT INTO order_coupon (order_id, coupon_id, discount_amount, created_at, updated_at) + VALUES (#{orderDetailId}, #{couponId}, #{discountAmount}, + #{createdAt}, #{updatedAt}); + + \ No newline at end of file diff --git a/src/main/resources/mapper/OrderDetailMapper.xml b/src/main/resources/mapper/OrderDetailMapper.xml new file mode 100644 index 0000000..5b21e39 --- /dev/null +++ b/src/main/resources/mapper/OrderDetailMapper.xml @@ -0,0 +1,13 @@ + + + + + + + INSERT INTO order_detail (product_id, quantity, price_per_each, total_order_price, total_discount_price, + total_payment_price, created_at, updated_at) + VALUES (#{productId}, #{quantity}, #{pricePerEach}, #{totalOrderPrice}, + #{totalDiscountPrice}, #{totalPaymentPrice}, #{createdAt}, #{updatedAt}); + + \ No newline at end of file diff --git a/src/main/resources/mapper/ProductMapper.xml b/src/main/resources/mapper/ProductMapper.xml index f1936bf..986485c 100644 --- a/src/main/resources/mapper/ProductMapper.xml +++ b/src/main/resources/mapper/ProductMapper.xml @@ -9,4 +9,10 @@ FROM product WHERE id = #{productId}; + + \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 7ea9dd5..d6f4f08 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -3,6 +3,7 @@ DROP TABLE IF EXISTS coupon; DROP TABLE IF EXISTS event; DROP TABLE IF EXISTS coupon_issue; DROP TABLE IF EXISTS product; +DROP TABLE IF EXISTS order_detail; CREATE TABLE member ( @@ -65,6 +66,7 @@ CREATE TABLE product description VARCHAR(255) NOT NULL COMMENT '상품 설명', original_price BIGINT UNSIGNED NOT NULL COMMENT '원래 상품 가격', sale_price BIGINT UNSIGNED NULL COMMENT '할인 상품 가격(쿠폰과 관계없이 전체적으로 할인할 경우)', + min_order_price BIGINT UNSIGNED NULL COMMENT '최소 주문 가격', created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); @@ -77,7 +79,7 @@ CREATE TABLE order_detail quantity BIGINT UNSIGNED NOT NULL COMMENT '상품 주문 수량', price_per_each BIGINT UNSIGNED NOT NULL COMMENT '상품 개당 가격', total_order_price BIGINT UNSIGNED NOT NULL COMMENT '총 상품 주문 가격', - discount_price BIGINT UNSIGNED NOT NULL COMMENT '할인 가격', + total_discount_price BIGINT UNSIGNED NOT NULL COMMENT '총 할인 가격', total_payment_price BIGINT UNSIGNED NOT NULL COMMENT '총 결제 가격', created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일' diff --git a/src/test/java/com/flab/offcoupon/service/coupon_issue/async/AsyncDefaultCouponIssueServiceTest.java b/src/test/java/com/flab/offcoupon/service/coupon_issue/async/AsyncDefaultCouponIssueServiceTest.java index d8d69a3..b03ff7e 100644 --- a/src/test/java/com/flab/offcoupon/service/coupon_issue/async/AsyncDefaultCouponIssueServiceTest.java +++ b/src/test/java/com/flab/offcoupon/service/coupon_issue/async/AsyncDefaultCouponIssueServiceTest.java @@ -78,8 +78,7 @@ void issueCoupon_fail_with_run_out_of_coupon() { LocalDateTime currentDateTime = LocalDateTime.now().withHour(13).withMinute(0).withSecond(0); long memberId = 1000L; long couponId = 1L; - Coupon coupon = couponRepository.findCouponById(couponId) - .orElseThrow(() -> new CouponNotFoundException(COUPON_NOT_EXIST.formatted(couponId)));; + Coupon coupon = couponRepository.getCouponById(couponId); LongStream.range(0L, coupon.getMaxQuantity()).forEach(idx -> { redisTemplate.opsForSet().add(getIssueRequestKey(coupon.getId()), String.valueOf(idx)); @@ -98,8 +97,7 @@ void issueCoupon_fail_with_duplicated_user() { LocalDateTime currentDateTime = LocalDateTime.now().withHour(13).withMinute(0).withSecond(0); long memberId = 1L; long couponId = 1L; - Coupon coupon = couponRepository.findCouponById(couponId) - .orElseThrow(() -> new CouponNotFoundException(COUPON_NOT_EXIST.formatted(couponId))); + Coupon coupon = couponRepository.getCouponById(couponId); redisTemplate.opsForSet().add(getIssueRequestKey(coupon.getId()), String.valueOf(memberId)); // when & then DuplicatedCouponException exception = Assertions.assertThrows(DuplicatedCouponException.class, () -> { diff --git a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/LettuceLockCouponIssueConcurrencyTest.java b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/LettuceLockCouponIssueConcurrencyTest.java index 3da08f7..2f95046 100644 --- a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/LettuceLockCouponIssueConcurrencyTest.java +++ b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/LettuceLockCouponIssueConcurrencyTest.java @@ -50,7 +50,7 @@ class LettuceLockCouponIssueConcurrencyTest { } latch.await(); - Coupon coupon = couponRepository.findCouponById(1).orElseThrow(); + Coupon coupon = couponRepository.getCouponById(1); boolean actualTransactionActive3 = TransactionSynchronizationManager.isActualTransactionActive(); System.out.println("끝 actualTransactionActive = " + actualTransactionActive3); diff --git a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/NamedLockCouponIssueConcurrencyTest.java b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/NamedLockCouponIssueConcurrencyTest.java index 4a639db..2c73d7a 100644 --- a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/NamedLockCouponIssueConcurrencyTest.java +++ b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/NamedLockCouponIssueConcurrencyTest.java @@ -46,7 +46,7 @@ class NamedLockCouponIssueConcurrencyTest { } latch.await(); - Coupon coupon = couponRepository.findCouponById(1).orElseThrow(); + Coupon coupon = couponRepository.getCouponById(1); // 500 - 100 == 400 assertEquals(400,coupon.remainedCoupon()); } diff --git a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/PessimisticLockCouponIssueConcurrencyTest.java b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/PessimisticLockCouponIssueConcurrencyTest.java index 0a91e7e..7e22a7c 100644 --- a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/PessimisticLockCouponIssueConcurrencyTest.java +++ b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/PessimisticLockCouponIssueConcurrencyTest.java @@ -48,7 +48,7 @@ class PessimisticLockCouponIssueConcurrencyTest { } latch.await(); - Coupon coupon = couponRepository.findCouponById(1).orElseThrow(); + Coupon coupon = couponRepository.getCouponById(1); // 500 - 100 == 400 assertEquals(400,coupon.remainedCoupon()); } diff --git a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/RedissonLockCouponIssueConcurrencyTest.java b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/RedissonLockCouponIssueConcurrencyTest.java index 5d6c9d3..48a6f46 100644 --- a/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/RedissonLockCouponIssueConcurrencyTest.java +++ b/src/test/java/com/flab/offcoupon/service/coupon_issue/concurrency/RedissonLockCouponIssueConcurrencyTest.java @@ -46,7 +46,7 @@ class RedissonLockCouponIssueConcurrencyTest { } latch.await(); - Coupon coupon = couponRepository.findCouponById(1).orElseThrow(); + Coupon coupon = couponRepository.getCouponById(1); // 500 - 100 == 400 assertEquals(400,coupon.remainedCoupon()); } From c844d640481b3c8437e8085ed07f2fc56821ee7c Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 18:31:52 +0900 Subject: [PATCH 08/27] =?UTF-8?q?refactor=20:=20list=20empty=EC=B2=B4?= =?UTF-8?q?=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/coupon_use/OrderService.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 862ef10..fee7646 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -38,15 +38,17 @@ public ResponseDTO> getAvailableCoupons List availableCoupons = couponIssueRepository.getAvailableCoupons(new MemberIdProductIdNowVo(memberId, productId, now)); List responseList = new ArrayList<>(); - long totalOrderPrice = 0; - // min_price(상품의 최소 주문 금액)과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. // TODO : NULL이 아니라 - for (AvailableCouponsByMemberIdVo availableCoupon : availableCoupons) { - // 최소 주문 금액까지 할인 가능한 금액 누적 - totalOrderPrice += availableCoupon.discountedPrice(); - // 할인 금액이 min_price보다 작거나 같으면 결과 리스트에 추가 - if (totalOrderPrice <= productRepository.getProductMinOrderPriceById(productId)) { - AvailableCouponsByMemberIdResponse response = new AvailableCouponsByMemberIdResponse(availableCoupon); - responseList.add(response); + // min_price(상품의 최소 주문 금액)과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. + if(!availableCoupons.isEmpty()) { + long totalOrderPrice = 0; + for (AvailableCouponsByMemberIdVo availableCoupon : availableCoupons) { + // 최소 주문 금액까지 할인 가능한 금액 누적 + totalOrderPrice += availableCoupon.discountedPrice(); + // 할인 금액이 min_price보다 작거나 같으면 결과 리스트에 추가 + if (totalOrderPrice <= productRepository.getProductMinOrderPriceById(productId)) { + AvailableCouponsByMemberIdResponse response = new AvailableCouponsByMemberIdResponse(availableCoupon); + responseList.add(response); + } } } return ResponseDTO.getSuccessResult(responseList); From 53c23e227f770974721a10827c7a4633b2c8a958 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 18:42:16 +0900 Subject: [PATCH 09/27] =?UTF-8?q?refactor=20:=20=EC=BF=A0=ED=8F=B0=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EA=B2=80=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=9D=B5=EC=85=89=EC=85=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offcoupon/controller/OrderController.java | 3 +-- .../order/ValidateCouponPeriodVo.java | 16 ---------------- .../exception/coupon/CouponErrorMessage.java | 3 ++- .../coupon/CouponStatusException.java | 15 +++++++++++++++ .../CouponUsageInvalidPeriodException.java | 15 +++++++++++++++ .../service/coupon_use/OrderService.java | 19 +++++++++++++++---- 6 files changed, 48 insertions(+), 23 deletions(-) delete mode 100644 src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateCouponPeriodVo.java create mode 100644 src/main/java/com/flab/offcoupon/exception/coupon/CouponStatusException.java create mode 100644 src/main/java/com/flab/offcoupon/exception/coupon/CouponUsageInvalidPeriodException.java diff --git a/src/main/java/com/flab/offcoupon/controller/OrderController.java b/src/main/java/com/flab/offcoupon/controller/OrderController.java index f1b376e..ccb7da7 100644 --- a/src/main/java/com/flab/offcoupon/controller/OrderController.java +++ b/src/main/java/com/flab/offcoupon/controller/OrderController.java @@ -47,7 +47,6 @@ public ResponseEntity>> get public ResponseEntity> orderProduct(@PathVariable final long productId, @RequestBody final OrderProductRequest orderProductRequest) { LocalDateTime now = LocalDateTime.now(); - orderService.orderProduct(productId,orderProductRequest, now); - return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.getSuccessResult("주문이 완료되었습니다.")); + return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.getSuccessResult(orderService.orderProduct(productId,orderProductRequest, now))); } } diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateCouponPeriodVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateCouponPeriodVo.java deleted file mode 100644 index d6efb25..0000000 --- a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateCouponPeriodVo.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.flab.offcoupon.domain.vo.persistence.order; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * MyBatis에서 여러개의 파라미터를 전달하기 위한 VO - * - * @param couponId - * @param currentDateTime - */ -public record ValidateCouponPeriodVo ( - List couponIds, - LocalDateTime currentDateTime -){ -} diff --git a/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java b/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java index 6f46fab..3a268d7 100644 --- a/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java +++ b/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java @@ -13,5 +13,6 @@ public class CouponErrorMessage { public static final String DUPLICATED_COUPON = "이미 발급된 쿠폰입니다. memberId : %s, couponId : %s"; public static final String ASYNC_DUPLICATED_COUPON = "이미 발급 요청이 처리됐습니다. memberId : %s, couponId : %s"; public static final String FAIL_COUPON_ISSUE_REQUEST = "쿠폰 발급에 실패했습니다. input: %s"; - + public static final String COUPON_USAGE_INVALID_PERIOD = "쿠폰의 유효기간 범위에 있지 않습니다. couponId: %s, validateStartDate: %s, validateEndDate : %s \""; + public static final String COUPON_IS_NOT_ACTIVE = "쿠폰이 활성화 되어있지 않습니다. couponId : %s"; } diff --git a/src/main/java/com/flab/offcoupon/exception/coupon/CouponStatusException.java b/src/main/java/com/flab/offcoupon/exception/coupon/CouponStatusException.java new file mode 100644 index 0000000..75a03ca --- /dev/null +++ b/src/main/java/com/flab/offcoupon/exception/coupon/CouponStatusException.java @@ -0,0 +1,15 @@ +package com.flab.offcoupon.exception.coupon; + +import com.flab.offcoupon.exception.CustomException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CouponStatusException extends CustomException { + public CouponStatusException(String message) { + super(message); + } +} diff --git a/src/main/java/com/flab/offcoupon/exception/coupon/CouponUsageInvalidPeriodException.java b/src/main/java/com/flab/offcoupon/exception/coupon/CouponUsageInvalidPeriodException.java new file mode 100644 index 0000000..8afa3cc --- /dev/null +++ b/src/main/java/com/flab/offcoupon/exception/coupon/CouponUsageInvalidPeriodException.java @@ -0,0 +1,15 @@ +package com.flab.offcoupon.exception.coupon; + +import com.flab.offcoupon.exception.CustomException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CouponUsageInvalidPeriodException extends CustomException { + public CouponUsageInvalidPeriodException(String message) { + super(message); + } +} diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index fee7646..97b31fc 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -9,6 +9,8 @@ import com.flab.offcoupon.domain.vo.persistence.order.ValidateNowIsBetweenPeriodVo; import com.flab.offcoupon.dto.request.OrderProductRequest; import com.flab.offcoupon.dto.response.AvailableCouponsByMemberIdResponse; +import com.flab.offcoupon.exception.coupon.CouponStatusException; +import com.flab.offcoupon.exception.coupon.CouponUsageInvalidPeriodException; import com.flab.offcoupon.repository.mysql.*; import com.flab.offcoupon.util.ResponseDTO; import lombok.RequiredArgsConstructor; @@ -22,6 +24,8 @@ import static com.flab.offcoupon.domain.entity.OrderCoupon.createOrderCoupon; import static com.flab.offcoupon.domain.entity.OrderDetail.createOrderDetail; import static com.flab.offcoupon.domain.entity.params.OrderInfo.createOrderInfo; +import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_IS_NOT_ACTIVE; +import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_USAGE_INVALID_PERIOD; @RequiredArgsConstructor @Service @@ -54,8 +58,15 @@ public ResponseDTO> getAvailableCoupons return ResponseDTO.getSuccessResult(responseList); } + /** + * 상품 주문 및 쿠폰 사용 처리 + * + * @param productId 상품 ID + * @param request 주문 요청 정보 + * @param now 현재 시간 + */ @Transactional - public void orderProduct(final long productId, final OrderProductRequest request, LocalDateTime now) { + public ResponseDTO orderProduct(final long productId, final OrderProductRequest request, LocalDateTime now) { validateCouponIsAvailable(request, now); // 3. 주문 정보 저장, 주문에 사용된 쿠폰 저장 OrderInfo orderInfo = createOrderInfo( @@ -73,6 +84,7 @@ public void orderProduct(final long productId, final OrderProductRequest request // 5. 쿠폰 사용 처리 couponIssueRepository.updateCouponStatus(request.getCouponIssueId()); + return ResponseDTO.getSuccessResult("쿠폰 처리 및 주문이 완료되었습니다."); } /** @@ -84,10 +96,9 @@ public void orderProduct(final long productId, final OrderProductRequest request private void validateCouponIsAvailable(OrderProductRequest request, LocalDateTime now) { // 1. 쿠폰들의 상태가 ACTIVE인지 확인 List couponIssueStatus = couponIssueRepository.validateStatusIsActive(request.getCouponIssueId()); - System.out.println("couponIssueStatus : " + couponIssueStatus); for (CouponIssuesAreActiveVo couponIssue : couponIssueStatus) { if (!couponIssue.isActive()) { - throw new IllegalArgumentException("쿠폰의 상태가 ACTIVE가 아닙니다. couponIssueId: " + couponIssue.couponIssueId()); + throw new CouponStatusException(COUPON_IS_NOT_ACTIVE.formatted(couponIssue.couponIssueId())); } } @@ -95,7 +106,7 @@ private void validateCouponIsAvailable(OrderProductRequest request, LocalDateTim List isBetweenValidatePeriodVo = couponRepository.validateNowIsBetweenPeriod(request.getCouponId(), now); for (ValidateNowIsBetweenPeriodVo isBetweenValidatePeriod : isBetweenValidatePeriodVo) { if (!isBetweenValidatePeriod.isBetweenValidatePeriod()) { - throw new IllegalArgumentException("쿠폰의 유효기간이 아닙니다. couponId: %s, validateStartDate: %s, validateEndDate : %s " + throw new CouponUsageInvalidPeriodException(COUPON_USAGE_INVALID_PERIOD .formatted(isBetweenValidatePeriod.couponId(), isBetweenValidatePeriod.validateStartDate(), isBetweenValidatePeriod.validateEndDate())); } } From c36b34d7a57c4eadb2528249c54c088c3cd556a2 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 18:44:34 +0900 Subject: [PATCH 10/27] =?UTF-8?q?refactor=20:=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=BF=A0=ED=8F=B0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/coupon_use/OrderService.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 97b31fc..cff1d1e 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -37,13 +37,32 @@ public class OrderService { private final OrderDetailRepository orderDetailRepository; private final OrderCouponRepository orderCouponRepository; + /** + * 사용 가능한 쿠폰 목록 조회 + * @param memberId 회원 ID + * @param productId 상품 ID + * @param now 현재 날짜 + * @return 사용 가능한 쿠폰 목록 + */ @Transactional(readOnly = true) public ResponseDTO> getAvailableCoupons(final long memberId, final long productId, final LocalDateTime now) { List availableCoupons = couponIssueRepository.getAvailableCoupons(new MemberIdProductIdNowVo(memberId, productId, now)); List responseList = new ArrayList<>(); + filterAvailableCouponsWithMinPrice(productId, availableCoupons, responseList); + return ResponseDTO.getSuccessResult(responseList); + } + + /** + * 최소 주문 금액과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. + * + * @param productId 상품 ID + * @param availableCoupons 사용 가능한 쿠폰 목록 + * @param responseList 결과 리스트 + */ + private void filterAvailableCouponsWithMinPrice(long productId, List availableCoupons, List responseList) { // min_price(상품의 최소 주문 금액)과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. - if(!availableCoupons.isEmpty()) { + if (!availableCoupons.isEmpty()) { long totalOrderPrice = 0; for (AvailableCouponsByMemberIdVo availableCoupon : availableCoupons) { // 최소 주문 금액까지 할인 가능한 금액 누적 @@ -55,15 +74,14 @@ public ResponseDTO> getAvailableCoupons } } } - return ResponseDTO.getSuccessResult(responseList); } /** * 상품 주문 및 쿠폰 사용 처리 * * @param productId 상품 ID - * @param request 주문 요청 정보 - * @param now 현재 시간 + * @param request 주문 요청 정보 + * @param now 현재 시간 */ @Transactional public ResponseDTO orderProduct(final long productId, final OrderProductRequest request, LocalDateTime now) { From 767f1bb5914e28abb77b24e733d1eaa0ffad31ae Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 22:05:29 +0900 Subject: [PATCH 11/27] =?UTF-8?q?refactor=20:=20java=20doc=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offcoupon/controller/OrderController.java | 2 +- .../flab/offcoupon/util/DiscountUtils.java | 51 ++++++++++++------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/controller/OrderController.java b/src/main/java/com/flab/offcoupon/controller/OrderController.java index ccb7da7..d1733ff 100644 --- a/src/main/java/com/flab/offcoupon/controller/OrderController.java +++ b/src/main/java/com/flab/offcoupon/controller/OrderController.java @@ -40,7 +40,7 @@ public ResponseEntity>> get * * @param productId 상품 ID * @param orderProductRequest 주문 요청 정보 - * @return + * @return 주문 성공 여부 */ @ResponseStatus(HttpStatus.CREATED) @PostMapping("/products/{productId}") diff --git a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java index 8d589c0..afa3c8f 100644 --- a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java +++ b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java @@ -31,23 +31,39 @@ public String getDiscount(DiscountType discountType, Long discountRate, Long dis } } + /** + * 상품의 가격을 반환하는 메소드입니다.
+ * 만약 세일 가격이 없다면 원래 가격을 반환합니다. + * + * @param product 상품 정보 + * @return 상품의 가격 + */ public long pricePerEach(Product product) { return product.getSalePrice() == null ? product.getOriginalPrice().longValue() : product.getSalePrice().longValue(); } + /** + * 상품의 총 가격을 반환하는 메소드입니다. + * @param product 상품 정보 + * @param quantity 상품 수량 + * @return + */ public long totalOrderPrice(Product product, long quantity) { return pricePerEach(product) * quantity; } + /** + * 상품의 총 할인 가격을 반환하는 메소드입니다. + * @param product 상품 정보 + * @param couponList 쿠폰 목록 + * @param quantity 상품 수량 + * @return 총 할인 가격 + */ public long totalDiscountPrice(Product product, List couponList, long quantity) { long totalDiscountPrice = 0; for (Coupon coupon : couponList) { if (coupon.getDiscountType() == DiscountType.PERCENT) { - double test = pricePerEach(product) * ((double)coupon.getDiscountRate() / 100); - System.out.println("pricePerEach: " + pricePerEach(product)); - System.out.println("coupon.getDiscountRate() : " + coupon.getDiscountRate()); - System.out.println("(coupon.getDiscountRate() / 100 : " + (coupon.getDiscountRate() / 100)); - System.out.println("test: " + test); + double test = pricePerEach(product) * ((double) coupon.getDiscountRate() / 100); totalDiscountPrice += test; } else { totalDiscountPrice += coupon.getDiscountPrice(); @@ -56,18 +72,12 @@ public long totalDiscountPrice(Product product, List couponList, long qu return totalDiscountPrice * quantity; } - public long calculateEachDiscountPrice(Product product, List couponList) { - long totalDiscountPrice = 0; - for (Coupon coupon : couponList) { - if (coupon.getDiscountType() == DiscountType.PERCENT) { - totalDiscountPrice += pricePerEach(product) * (coupon.getDiscountRate() / 100); - } else { - totalDiscountPrice += coupon.getDiscountPrice(); - } - } - return totalDiscountPrice; - } - + /** + * 상품의 개당 할인 가격을 반환하는 메소드입니다. + * @param product 상품 정보 + * @param coupon 쿠폰 정보 + * @return 개당 할인 가격 + */ public long calculateEachDiscountPrice(Product product, Coupon coupon) { long totalDiscountPrice = 0; @@ -80,6 +90,13 @@ public long calculateEachDiscountPrice(Product product, Coupon coupon) { return totalDiscountPrice; } + /** + * 상품의 총 결제 가격을 반환하는 메소드입니다. + * @param product 상품 정보 + * @param quantity 상품 수량 + * @param couponList 쿠폰 목록 + * @return 총 결제 가격 + */ public long totalPaymentPrice(Product product, long quantity, List couponList) { return totalOrderPrice(product, quantity) - totalDiscountPrice(product, couponList, quantity); } From d8c047f0486cd0b8a7ff993497677365459a22ae Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 22:56:40 +0900 Subject: [PATCH 12/27] =?UTF-8?q?refactor=20:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/configs/SecurityConfig.java | 2 +- .../offcoupon/controller/MyPageController.java | 5 ----- .../offcoupon/dto/request/OrderProductRequest.java | 1 - .../exception/coupon/CouponErrorMessage.java | 2 +- .../exception/coupon/CouponExceptionHandler.java | 14 ++++++++++++++ .../offcoupon/service/coupon_use/OrderService.java | 2 +- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java index c095e2e..fe380d2 100644 --- a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java +++ b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java @@ -48,7 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/v1/members/signup", "/main", "/api/v1/sse/**", - "/api/v1/purchase/**", + "/api/v1/my-page/**", "/api/v1/orders/**", "/api/v1/event/**") .permitAll() diff --git a/src/main/java/com/flab/offcoupon/controller/MyPageController.java b/src/main/java/com/flab/offcoupon/controller/MyPageController.java index 335199d..f642cdf 100644 --- a/src/main/java/com/flab/offcoupon/controller/MyPageController.java +++ b/src/main/java/com/flab/offcoupon/controller/MyPageController.java @@ -21,9 +21,4 @@ public class MyPageController { public ResponseEntity>> getAllCoupons(@RequestParam final long memberId) { return ResponseEntity.status(HttpStatus.OK).body(myPageService.getAllCoupons(memberId)); } - -// @GetMapping("/coupon-use") -// public void getAvailableCoupons(@RequestParam final long memberId) { -// couponUseService.getAvailableCoupons(memberId); -// } } diff --git a/src/main/java/com/flab/offcoupon/dto/request/OrderProductRequest.java b/src/main/java/com/flab/offcoupon/dto/request/OrderProductRequest.java index f4d2049..90c8b28 100644 --- a/src/main/java/com/flab/offcoupon/dto/request/OrderProductRequest.java +++ b/src/main/java/com/flab/offcoupon/dto/request/OrderProductRequest.java @@ -10,7 +10,6 @@ @Getter @RequiredArgsConstructor public final class OrderProductRequest { - private final List couponIssueId; // 하나의 주문에 여러개의 쿠폰을 사용할 경우 private final List couponId; // 하나의 주문에 여러개의 쿠폰을 사용할 경우 private final int quantity; // 주문할 상품 수량 diff --git a/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java b/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java index 3a268d7..8904898 100644 --- a/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java +++ b/src/main/java/com/flab/offcoupon/exception/coupon/CouponErrorMessage.java @@ -13,6 +13,6 @@ public class CouponErrorMessage { public static final String DUPLICATED_COUPON = "이미 발급된 쿠폰입니다. memberId : %s, couponId : %s"; public static final String ASYNC_DUPLICATED_COUPON = "이미 발급 요청이 처리됐습니다. memberId : %s, couponId : %s"; public static final String FAIL_COUPON_ISSUE_REQUEST = "쿠폰 발급에 실패했습니다. input: %s"; - public static final String COUPON_USAGE_INVALID_PERIOD = "쿠폰의 유효기간 범위에 있지 않습니다. couponId: %s, validateStartDate: %s, validateEndDate : %s \""; + public static final String COUPON_USAGE_INVALID_PERIOD = "쿠폰의 유효기간 범위에 있지 않습니다. couponId: %s, validateStartDate: %s, validateEndDate : %s "; public static final String COUPON_IS_NOT_ACTIVE = "쿠폰이 활성화 되어있지 않습니다. couponId : %s"; } diff --git a/src/main/java/com/flab/offcoupon/exception/coupon/CouponExceptionHandler.java b/src/main/java/com/flab/offcoupon/exception/coupon/CouponExceptionHandler.java index ce4969c..7c7908a 100644 --- a/src/main/java/com/flab/offcoupon/exception/coupon/CouponExceptionHandler.java +++ b/src/main/java/com/flab/offcoupon/exception/coupon/CouponExceptionHandler.java @@ -33,4 +33,18 @@ public ResponseEntity> couponBadRequestException(DuplicatedC ex.getMessage(), HttpStatus.BAD_REQUEST); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDTO.getFailResult(ex.getMessage())); } + + @ExceptionHandler(CouponStatusException.class) + public ResponseEntity> couponBadRequestException(CouponStatusException ex, HttpServletRequest request) { + log.info(HTTP_REQUEST, request.getMethod(), request.getRequestURI(), + ex.getMessage(), HttpStatus.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDTO.getFailResult(ex.getMessage())); + } + + @ExceptionHandler(CouponUsageInvalidPeriodException.class) + public ResponseEntity> couponBadRequestException(CouponUsageInvalidPeriodException ex, HttpServletRequest request) { + log.info(HTTP_REQUEST, request.getMethod(), request.getRequestURI(), + ex.getMessage(), HttpStatus.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDTO.getFailResult(ex.getMessage())); + } } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index cff1d1e..10fb713 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -86,7 +86,7 @@ private void filterAvailableCouponsWithMinPrice(long productId, List orderProduct(final long productId, final OrderProductRequest request, LocalDateTime now) { validateCouponIsAvailable(request, now); - // 3. 주문 정보 저장, 주문에 사용된 쿠폰 저장 + // 3. 주문 정보 저장 OrderInfo orderInfo = createOrderInfo( productRepository.getProductById(productId), request.getQuantity(), From fca2d3c96b57162f66f787db5864486fba22ac2d Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 8 Apr 2024 23:07:42 +0900 Subject: [PATCH 13/27] =?UTF-8?q?refactor=20:=20QueryTest=20@Value?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=B0=A9=EB=B2=95=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=AA=BB=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 1 + .../java/com/flab/offcoupon/QueryTest.java | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a7d5a4b..d790a70 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,6 +14,7 @@ spring.datasource.url=jdbc:log4jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_DATABASE} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} +spring.datasource.url.query_test=jdbc:mysql://localhost/off_coupon #spring.sql.init.schema-locations=classpath:schema.sql,classpath:data.sql #spring.sql.init.mode=always diff --git a/src/test/java/com/flab/offcoupon/QueryTest.java b/src/test/java/com/flab/offcoupon/QueryTest.java index 64de57d..549592c 100644 --- a/src/test/java/com/flab/offcoupon/QueryTest.java +++ b/src/test/java/com/flab/offcoupon/QueryTest.java @@ -1,31 +1,33 @@ package com.flab.offcoupon; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Value; import java.sql.*; import static org.junit.jupiter.api.Assertions.assertTrue; +@Disabled("QueryTest는 쿼리 최적화 성능테스트용이므로 비활성화") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class QueryTest { - - private static final String url = "jdbc:mysql://localhost/off_coupon"; - private static final String user = "root"; - private static final String password = "1234"; + @Value("${spring.datasource.url.query_test}") // TODO : Junit5에서 @Value사용할 수 있는 방법 알아보기 + private String url; + @Value("${spring.datasource.username}") // TODO : Junit5에서 @Value사용할 수 있는 방법 알아보기 + private String user; + @Value("${spring.datasource.password}") // TODO : Junit5에서 @Value사용할 수 있는 방법 알아보기 + private String password; private static final int queryCount = 10; private static long totalExecutionTimeWithIndex = 0; private static long totalExecutionTimeWithoutIndex = 0; private static Connection connection; @BeforeAll - static void setUpBeforeClass() throws Exception { + void setUpBeforeClass() throws Exception { connection = DriverManager.getConnection(url, user, password); } @AfterAll - static void tearDownAfterClass() throws Exception { + void tearDownAfterClass() throws Exception { if (connection != null) { connection.close(); } @@ -33,7 +35,7 @@ static void tearDownAfterClass() throws Exception { @Test @DisplayName("[쿼리 실행 시간 측정] 인덱스 있는 쿼리 실행 시간 비교") - public void testQueryWithIndex() throws SQLException { + void testQueryWithIndex() throws SQLException { for (int i = 0; i < queryCount; i++) { long startTime = System.currentTimeMillis(); // 시작 시간 기록 try (Statement statement = connection.createStatement()) { @@ -50,7 +52,7 @@ public void testQueryWithIndex() throws SQLException { @Test @DisplayName("[쿼리 실행 시간 측정] 인덱스 없는 쿼리 실행 시간 비교") - public void testQueryWithoutIndex() throws SQLException { + void testQueryWithoutIndex() throws SQLException { for (int i = 0; i < queryCount; i++) { long startTime = System.currentTimeMillis(); // 시작 시간 기록 try (Statement statement = connection.createStatement()) { @@ -67,7 +69,7 @@ public void testQueryWithoutIndex() throws SQLException { @Test @DisplayName("[평균 실행 시간 계산] 인덱스 생성 전후 쿼리 실행 시간 비교") - public void testAverageExecutionTime() { + void testAverageExecutionTime() { // 평균 실행 시간 계산 double averageExecutionTimeWithIndex = (double) totalExecutionTimeWithIndex / queryCount; double averageExecutionTimeWithoutIndex = (double) totalExecutionTimeWithoutIndex / queryCount; From 3c82ef269b4edfd89df18425f25d27ec649d361e Mon Sep 17 00:00:00 2001 From: sejin park Date: Tue, 9 Apr 2024 10:51:20 +0900 Subject: [PATCH 14/27] =?UTF-8?q?refactor=20:=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index d6f4f08..ff11bb9 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -8,11 +8,11 @@ DROP TABLE IF EXISTS order_detail; CREATE TABLE member ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '회원 식별자', - email VARCHAR(40) NOT NULL COMMENT '회원 이메일', + email VARCHAR(255) NOT NULL COMMENT '회원 이메일', password VARCHAR(255) NOT NULL COMMENT '비밀번호', - name VARCHAR(40) NOT NULL COMMENT '회원 이름', + name VARCHAR(100) NOT NULL COMMENT '회원 이름', birthdate DATE NOT NULL COMMENT '생년월일', - phone VARCHAR(40) NOT NULL COMMENT '휴대폰 번호', + phone VARCHAR(50) NOT NULL COMMENT '휴대폰 번호', role VARCHAR(50) NOT NULL COMMENT '회원 권한', created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일' @@ -35,10 +35,10 @@ CREATE TABLE coupon ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰 식별자', event_id BIGINT UNSIGNED NULL COMMENT '이벤트 식별자 / NULL일 경우 이벤트와 관련 없는 쿠폰(e.g. 회원가입 쿠폰)', - discount_type VARCHAR(40) NOT NULL COMMENT '정액, 정률 등', + discount_type VARCHAR(50) NOT NULL COMMENT '정액, 정률 등', discount_rate BIGINT UNSIGNED NULL COMMENT '정률 할인', discount_price BIGINT UNSIGNED NULL COMMENT '정액 할인', - coupon_type VARCHAR(50) NOT NULL COMMENT '선착순 쿠폰, 회원가입 쿠폰 등..', + coupon_type VARCHAR(100) NOT NULL COMMENT '선착순 쿠폰, 회원가입 쿠폰 등..', max_quantity BIGINT UNSIGNED NULL COMMENT '무제한 발행일 경우 NULL', issued_quantity BIGINT UNSIGNED NULL COMMENT '무제한 발행일 경우 NULL', validate_start_date DATETIME NOT NULL COMMENT '모든 쿠폰은 유효 시간이 있어야한다는 제약 사항 존재', @@ -61,8 +61,8 @@ CREATE TABLE coupon_issue_no_idx CREATE TABLE product ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '상품 식별자', - category VARCHAR(100) NOT NULL COMMENT '상품 카테고리', - title VARCHAR(100) NOT NULL COMMENT '상품명', + category VARCHAR(255) NOT NULL COMMENT '상품 카테고리', + title VARCHAR(255) NOT NULL COMMENT '상품명', description VARCHAR(255) NOT NULL COMMENT '상품 설명', original_price BIGINT UNSIGNED NOT NULL COMMENT '원래 상품 가격', sale_price BIGINT UNSIGNED NULL COMMENT '할인 상품 가격(쿠폰과 관계없이 전체적으로 할인할 경우)', @@ -89,7 +89,7 @@ CREATE TABLE order_detail CREATE TABLE order_coupon ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰을 사용한 주문 식별자', order_id BIGINT UNSIGNED NOT NULL COMMENT '주문 ID', - coupon_id VARCHAR(50) NOT NULL COMMENT '쿠폰 ID', + coupon_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', discount_amount BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 할인액', created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일' From c1b8be5369a4949095368b96df2933a3a96706ee Mon Sep 17 00:00:00 2001 From: sejin park Date: Tue, 9 Apr 2024 10:51:51 +0900 Subject: [PATCH 15/27] =?UTF-8?q?refactor=20:=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index ff11bb9..4e9c874 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -47,7 +47,7 @@ CREATE TABLE coupon updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); # 쿼리 최적화 성능 비교 테스트용 -CREATE TABLE coupon_issue_no_idx +CREATE TABLE coupon_issue ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰 발행 기록', member_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', From dc01f8be025cf530ca3ef08f659149f2a797d642 Mon Sep 17 00:00:00 2001 From: sejin park Date: Tue, 9 Apr 2024 10:51:59 +0900 Subject: [PATCH 16/27] =?UTF-8?q?refactor=20:=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 4e9c874..f553de8 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -46,7 +46,7 @@ CREATE TABLE coupon created_at DATETIME NOT NULL COMMENT '데이터 생성일', updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); -# 쿼리 최적화 성능 비교 테스트용 + CREATE TABLE coupon_issue ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰 발행 기록', From 2b0c15ea2a6187ff241425f4b01c2c5461cd8e66 Mon Sep 17 00:00:00 2001 From: sejin park Date: Tue, 9 Apr 2024 10:54:25 +0900 Subject: [PATCH 17/27] =?UTF-8?q?chore=20:=20properties=20Value=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20=EC=95=88=EB=90=98=EC=84=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=ED=95=98=EB=8A=90=EB=9D=BC=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=ED=95=9C=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d790a70..367dea1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,7 +14,7 @@ spring.datasource.url=jdbc:log4jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_DATABASE} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} -spring.datasource.url.query_test=jdbc:mysql://localhost/off_coupon +spring.datasource.url.query_test=${DB_QUERY_TEST} #spring.sql.init.schema-locations=classpath:schema.sql,classpath:data.sql #spring.sql.init.mode=always From 235238ab4eb56aa19286d0378dfa03eb523858cf Mon Sep 17 00:00:00 2001 From: sejin park Date: Tue, 9 Apr 2024 10:56:04 +0900 Subject: [PATCH 18/27] =?UTF-8?q?refactor=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=ED=95=98=EB=8A=90=EB=9D=BC=20@Scheduled=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B2=98=EB=A6=AC=ED=95=9C=EA=B1=B0=20=EC=9B=90?= =?UTF-8?q?=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon_issue/async/consumer/CouponIssueConsumer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java b/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java index c2bf5ed..bcbab37 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_issue/async/consumer/CouponIssueConsumer.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import static com.flab.offcoupon.util.CouponRabbitMQConstants.QUEUE_NAME; @@ -37,7 +38,7 @@ public class CouponIssueConsumer { /** * 3초마다 메시지 큐를 확인하여 메시지가 있는지 여부를 판단하고 쿠폰 이력을 INSERT합니다. */ - // @Scheduled(fixedDelay = 3000) + @Scheduled(fixedDelay = 3000) private void consumeCouponIssueMessage() { if (existCouponIssueQueueTarget()) { CouponIssueMessageForQueue message = (CouponIssueMessageForQueue) rabbitTemplate.receiveAndConvert(QUEUE_NAME); @@ -51,7 +52,7 @@ private void consumeCouponIssueMessage() { /** * 10초마다 오늘 발급된 쿠폰의 총 발급 수량을 조회해서 반정규화된 칼럼을 업데이트합니다. */ - // @Scheduled(fixedDelay = 10000) + @Scheduled(fixedDelay = 10000) private void updateTotalCouponIssueCount() { redissonLockHandler.asyncIssueCoupon(); } From cfbf82bb13076749ec2fd1fc54142c14183a8a4a Mon Sep 17 00:00:00 2001 From: sejin park Date: Tue, 9 Apr 2024 11:18:29 +0900 Subject: [PATCH 19/27] =?UTF-8?q?refactor=20:=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=EB=AA=85,=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=AA=85?= =?UTF-8?q?=ED=99=95=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flab/offcoupon/domain/entity/Product.java | 7 +-- .../entity/params/AppliedCouponInfo.java | 2 +- .../domain/entity/params/OrderInfo.java | 8 +-- .../service/coupon_use/OrderService.java | 6 +- .../flab/offcoupon/util/DiscountUtils.java | 62 ++++++++++--------- src/main/resources/schema.sql | 1 + 6 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/domain/entity/Product.java b/src/main/java/com/flab/offcoupon/domain/entity/Product.java index 098b8cb..09dfb73 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/Product.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/Product.java @@ -4,7 +4,6 @@ import lombok.Getter; import lombok.ToString; -import java.math.BigDecimal; import java.time.LocalDateTime; /** @@ -18,9 +17,9 @@ public final class Product { private final String category; private final String title; private final String description; - private final BigDecimal originalPrice; // 원래 가격 - private final BigDecimal salePrice; // null일 경우 전체 할인이 적용되지 않은 것으로 간주 - private final BigDecimal minOrderPrice; // 최소 주문 가격 + private final long originalPrice; // 원래 가격 + private final Long salePrice; // null일 경우 전체 할인이 적용되지 않은 것으로 간주 + private final long minOrderPrice; // 최소 주문 가격 private final LocalDateTime createdAt; private final LocalDateTime updatedAt; } diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java index 66cb7e4..cabc3bc 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java @@ -15,7 +15,7 @@ public final class AppliedCouponInfo { private AppliedCouponInfo(Product product, Coupon coupon) { this.couponId = coupon.getId(); - this.discountAmount = DiscountUtils.calculateEachDiscountPrice(product, coupon); + this.discountAmount = DiscountUtils.calculateDiscountPricePerUnit(product, coupon); } public static AppliedCouponInfo createAppliedCouponInfo(Product product, Coupon coupon) { diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java index 64981b7..e149309 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java @@ -22,11 +22,11 @@ public final class OrderInfo { private OrderInfo(Product product, long quantity, List couponList) { this.productId = product.getId(); this.quantity = quantity; - this.pricePerEach = DiscountUtils.pricePerEach(product); - this.totalOrderPrice = DiscountUtils.totalOrderPrice(product, quantity); + this.pricePerEach = DiscountUtils.getProductPricePerUnit(product); + this.totalOrderPrice = DiscountUtils.calculateTotalPrice(product, quantity); this.appliedCouponInfos = createAppliedCouponInfos(product, couponList); - this.totalDiscountPrice = DiscountUtils.totalDiscountPrice(product, couponList, quantity); - this.totalPaymentPrice = DiscountUtils.totalPaymentPrice(product, quantity, couponList); + this.totalDiscountPrice = DiscountUtils.calculateTotalDiscountPrice(product, couponList, quantity); + this.totalPaymentPrice = DiscountUtils.calculateTotalPaymentPrice(product, quantity, couponList); } public static OrderInfo createOrderInfo(Product product, long quantity, List couponList) { diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 10fb713..61f3181 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -63,12 +63,12 @@ public ResponseDTO> getAvailableCoupons private void filterAvailableCouponsWithMinPrice(long productId, List availableCoupons, List responseList) { // min_price(상품의 최소 주문 금액)과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. if (!availableCoupons.isEmpty()) { - long totalOrderPrice = 0; + long accumulatedDiscountPrice = 0; for (AvailableCouponsByMemberIdVo availableCoupon : availableCoupons) { // 최소 주문 금액까지 할인 가능한 금액 누적 - totalOrderPrice += availableCoupon.discountedPrice(); + accumulatedDiscountPrice += availableCoupon.discountedPrice(); // 할인 금액이 min_price보다 작거나 같으면 결과 리스트에 추가 - if (totalOrderPrice <= productRepository.getProductMinOrderPriceById(productId)) { + if (accumulatedDiscountPrice <= productRepository.getProductMinOrderPriceById(productId)) { AvailableCouponsByMemberIdResponse response = new AvailableCouponsByMemberIdResponse(availableCoupon); responseList.add(response); } diff --git a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java index afa3c8f..2706510 100644 --- a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java +++ b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java @@ -32,39 +32,40 @@ public String getDiscount(DiscountType discountType, Long discountRate, Long dis } /** - * 상품의 가격을 반환하는 메소드입니다.
+ * 상품의 개별 가격을 반환하는 메소드입니다.
* 만약 세일 가격이 없다면 원래 가격을 반환합니다. * * @param product 상품 정보 - * @return 상품의 가격 + * @return 상품의 개별 가격 */ - public long pricePerEach(Product product) { - return product.getSalePrice() == null ? product.getOriginalPrice().longValue() : product.getSalePrice().longValue(); + public long getProductPricePerUnit(Product product) { + return product.getSalePrice() == null ? product.getOriginalPrice() : product.getSalePrice(); } /** - * 상품의 총 가격을 반환하는 메소드입니다. - * @param product 상품 정보 + * 상품의 총 가격을 계산하여 반환하는 메소드입니다. + * + * @param product 상품 정보 * @param quantity 상품 수량 - * @return + * @return 상품의 총 가격 */ - public long totalOrderPrice(Product product, long quantity) { - return pricePerEach(product) * quantity; + public long calculateTotalPrice(Product product, long quantity) { + return getProductPricePerUnit(product) * quantity; } /** - * 상품의 총 할인 가격을 반환하는 메소드입니다. - * @param product 상품 정보 - * @param couponList 쿠폰 목록 - * @param quantity 상품 수량 + * 상품에 대한 총 할인 가격을 계산하여 반환하는 메소드입니다. + * + * @param product 상품 정보 + * @param couponList 쿠폰 목록 + * @param quantity 상품 수량 * @return 총 할인 가격 */ - public long totalDiscountPrice(Product product, List couponList, long quantity) { + public long calculateTotalDiscountPrice(Product product, List couponList, long quantity) { long totalDiscountPrice = 0; for (Coupon coupon : couponList) { if (coupon.getDiscountType() == DiscountType.PERCENT) { - double test = pricePerEach(product) * ((double) coupon.getDiscountRate() / 100); - totalDiscountPrice += test; + totalDiscountPrice += getProductPricePerUnit(product) * ((double) coupon.getDiscountRate() / 100); } else { totalDiscountPrice += coupon.getDiscountPrice(); } @@ -73,31 +74,34 @@ public long totalDiscountPrice(Product product, List couponList, long qu } /** - * 상품의 개당 할인 가격을 반환하는 메소드입니다. + * 상품의 개당 할인 가격을 계산하여 반환하는 메소드입니다. + * * @param product 상품 정보 - * @param coupon 쿠폰 정보 + * @param coupon 쿠폰 정보 * @return 개당 할인 가격 */ - public long calculateEachDiscountPrice(Product product, Coupon coupon) { - long totalDiscountPrice = 0; + public long calculateDiscountPricePerUnit(Product product, Coupon coupon) { + long discountPricePerUnit = 0; if (coupon.getDiscountType() == DiscountType.PERCENT) { - totalDiscountPrice += pricePerEach(product) * coupon.getDiscountRate() / 100; + discountPricePerUnit += getProductPricePerUnit(product) * coupon.getDiscountRate() / 100; } else { - totalDiscountPrice += coupon.getDiscountPrice(); + discountPricePerUnit += coupon.getDiscountPrice(); } - return totalDiscountPrice; + return discountPricePerUnit; } + /** - * 상품의 총 결제 가격을 반환하는 메소드입니다. - * @param product 상품 정보 - * @param quantity 상품 수량 - * @param couponList 쿠폰 목록 + * 상품에 대한 총 결제 가격을 계산하여 반환하는 메소드입니다. + * + * @param product 상품 정보 + * @param quantity 상품 수량 + * @param couponList 쿠폰 목록 * @return 총 결제 가격 */ - public long totalPaymentPrice(Product product, long quantity, List couponList) { - return totalOrderPrice(product, quantity) - totalDiscountPrice(product, couponList, quantity); + public long calculateTotalPaymentPrice(Product product, long quantity, List couponList) { + return calculateTotalPrice(product, quantity) - calculateTotalDiscountPrice(product, couponList, quantity); } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index f553de8..800cd34 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -4,6 +4,7 @@ DROP TABLE IF EXISTS event; DROP TABLE IF EXISTS coupon_issue; DROP TABLE IF EXISTS product; DROP TABLE IF EXISTS order_detail; +DROP TABLE IF EXISTS order_coupon; CREATE TABLE member ( From 450f9d6ddfe86f0aab805b392ffc88ddbc3d707e Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 12:44:12 +0900 Subject: [PATCH 20/27] =?UTF-8?q?refactor=20:=20Controller=EC=97=90?= =?UTF-8?q?=EC=84=9C=20@ResponseStatus=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/flab/offcoupon/controller/CouponIssueController.java | 2 -- .../java/com/flab/offcoupon/controller/MemberController.java | 1 - .../java/com/flab/offcoupon/controller/MyPageController.java | 1 - .../java/com/flab/offcoupon/controller/OrderController.java | 2 -- 4 files changed, 6 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java b/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java index 157651e..42a3de2 100644 --- a/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java +++ b/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java @@ -31,7 +31,6 @@ public class CouponIssueController { private final CouponIssueRequestService couponIssueRequestService; - @ResponseStatus(HttpStatus.CREATED) @PostMapping("/{eventId}/issues-sync") public ResponseEntity> syncIssue(@PathVariable final long eventId, @RequestParam final long couponId, @@ -39,7 +38,6 @@ public ResponseEntity> syncIssue(@PathVariable final long ev LocalDateTime currentDateTime = LocalDateTime.now(); return ResponseEntity.status(HttpStatus.CREATED).body(couponIssueRequestService.syncIssueCoupon(currentDateTime, eventId, couponId, memberId)); } - @ResponseStatus(HttpStatus.CREATED) @PostMapping("/{eventId}/issues-async") public ResponseEntity> asyncIssue(@PathVariable final long eventId, @RequestParam final long couponId, diff --git a/src/main/java/com/flab/offcoupon/controller/MemberController.java b/src/main/java/com/flab/offcoupon/controller/MemberController.java index 61eeb20..51c02ca 100644 --- a/src/main/java/com/flab/offcoupon/controller/MemberController.java +++ b/src/main/java/com/flab/offcoupon/controller/MemberController.java @@ -16,7 +16,6 @@ public class MemberController { private final MemberService memberService; - @ResponseStatus(HttpStatus.CREATED) @PostMapping("/signup") public ResponseEntity signup(@RequestBody @Valid final SignupMemberRequestDto signupMemberRequestDto) { return ResponseEntity.status(HttpStatus.CREATED).body(memberService.signUp(signupMemberRequestDto)); diff --git a/src/main/java/com/flab/offcoupon/controller/MyPageController.java b/src/main/java/com/flab/offcoupon/controller/MyPageController.java index f642cdf..d9f4d39 100644 --- a/src/main/java/com/flab/offcoupon/controller/MyPageController.java +++ b/src/main/java/com/flab/offcoupon/controller/MyPageController.java @@ -16,7 +16,6 @@ public class MyPageController { private final MyPageService myPageService; - @ResponseStatus(HttpStatus.OK) @GetMapping("/coupons") public ResponseEntity>> getAllCoupons(@RequestParam final long memberId) { return ResponseEntity.status(HttpStatus.OK).body(myPageService.getAllCoupons(memberId)); diff --git a/src/main/java/com/flab/offcoupon/controller/OrderController.java b/src/main/java/com/flab/offcoupon/controller/OrderController.java index d1733ff..7cdbbd2 100644 --- a/src/main/java/com/flab/offcoupon/controller/OrderController.java +++ b/src/main/java/com/flab/offcoupon/controller/OrderController.java @@ -26,7 +26,6 @@ public class OrderController { * @param productId 상품 ID * @return 사용 가능한 쿠폰 목록 */ - @ResponseStatus(HttpStatus.OK) @GetMapping("/available-coupons") public ResponseEntity>> getAvailableCoupons(@RequestParam final long memberId, @RequestParam final long productId) { @@ -42,7 +41,6 @@ public ResponseEntity>> get * @param orderProductRequest 주문 요청 정보 * @return 주문 성공 여부 */ - @ResponseStatus(HttpStatus.CREATED) @PostMapping("/products/{productId}") public ResponseEntity> orderProduct(@PathVariable final long productId, @RequestBody final OrderProductRequest orderProductRequest) { From 894f7c2d7a3ddcf5be166e9740b19b56bcd17922 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 14:04:01 +0900 Subject: [PATCH 21/27] =?UTF-8?q?refactor=20:=20Product=20Exception=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B0=80=EA=B2=A9=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20decimal=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offcoupon/domain/entity/OrderCoupon.java | 3 +- .../offcoupon/domain/entity/OrderDetail.java | 9 +- .../flab/offcoupon/domain/entity/Product.java | 88 ++++++++++++++++- .../entity/params/AppliedCouponInfo.java | 7 +- .../domain/entity/params/OrderInfo.java | 18 ++-- .../AllCouponsByMemberIdResponse.java | 2 +- .../AvailableCouponsByMemberIdResponse.java | 2 +- .../product/ProductErrorMessage.java | 9 ++ .../product/ProductExceptionHandler.java | 22 +++++ .../product/ProductNotFoundException.java | 15 +++ .../repository/mysql/ProductRepository.java | 5 +- .../flab/offcoupon/util/DiscountUtils.java | 94 ++----------------- src/main/resources/schema.sql | 91 +++++++++--------- 13 files changed, 213 insertions(+), 152 deletions(-) create mode 100644 src/main/java/com/flab/offcoupon/exception/product/ProductErrorMessage.java create mode 100644 src/main/java/com/flab/offcoupon/exception/product/ProductExceptionHandler.java create mode 100644 src/main/java/com/flab/offcoupon/exception/product/ProductNotFoundException.java diff --git a/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java b/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java index d6c8f17..483f78a 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.ToString; +import java.math.BigDecimal; import java.time.LocalDateTime; /** @@ -19,7 +20,7 @@ public final class OrderCoupon { private long id; private final long orderDetailId; private final long couponId; - private final long discountAmount; + private final BigDecimal discountAmount; private final LocalDateTime createdAt; private final LocalDateTime updatedAt; diff --git a/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java b/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java index 0ab30d7..a3c3c54 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.ToString; +import java.math.BigDecimal; import java.time.LocalDateTime; /** @@ -19,10 +20,10 @@ public final class OrderDetail { private long id; private final long productId; private final long quantity; - private final long pricePerEach; // 상품 1개당 가격 - private final long totalOrderPrice; // 총 상품 주문 가격 - private final long totalDiscountPrice; // 할인 가격 - private final long totalPaymentPrice; // 총 상품 주문 가격 - 할인 가격 + private final BigDecimal pricePerEach; // 상품 1개당 가격 + private final BigDecimal totalOrderPrice; // 총 상품 주문 가격 + private final BigDecimal totalDiscountPrice; // 할인 가격 + private final BigDecimal totalPaymentPrice; // 총 상품 주문 가격 - 할인 가격 private final LocalDateTime createdAt; private final LocalDateTime updatedAt; diff --git a/src/main/java/com/flab/offcoupon/domain/entity/Product.java b/src/main/java/com/flab/offcoupon/domain/entity/Product.java index 09dfb73..357445d 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/Product.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/Product.java @@ -4,7 +4,10 @@ import lombok.Getter; import lombok.ToString; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; +import java.util.List; /** * 상품 정보를 담는 도메인 객체입니다. @@ -17,9 +20,88 @@ public final class Product { private final String category; private final String title; private final String description; - private final long originalPrice; // 원래 가격 - private final Long salePrice; // null일 경우 전체 할인이 적용되지 않은 것으로 간주 - private final long minOrderPrice; // 최소 주문 가격 + private final BigDecimal originalPrice; // 원래 가격 + private final BigDecimal salePrice; // 0일 경우 전체 할인이 적용되지 않은 것으로 간주 + private final BigDecimal minOrderPrice; // 최소 주문 가격 private final LocalDateTime createdAt; private final LocalDateTime updatedAt; + + /** + * 상품의 개별 가격을 반환하는 메소드입니다.
+ * 만약 세일 가격이 0일 경우 원래 가격을 반환합니다. + * + * @return 상품의 개별 가격 + */ +// public BigDecimal getPricePerUnit(Product product) { +// return product.getSalePrice().compareTo(BigDecimal.ZERO) > 0 ? product.getSalePrice() : product.getOriginalPrice(); +// } + public BigDecimal getPricePerUnit() { + return this.salePrice.compareTo(BigDecimal.ZERO) > 0 ? salePrice : originalPrice; + } + + /** + * 상품의 총 가격을 계산하여 반환하는 메소드입니다. + * + * @param quantity 상품 수량 + * @return 상품의 총 가격 + */ + public BigDecimal calculateTotalPrice(long quantity) { + return getPricePerUnit().multiply(BigDecimal.valueOf(quantity)); + } + + + /** + * 상품에 대한 총 할인 가격을 계산하여 반환하는 메소드입니다. + * + * @param couponList 쿠폰 목록 + * @param quantity 상품 수량 + * @return 총 할인 가격 + */ + + public BigDecimal calculateTotalDiscountPrice(List couponList, long quantity) { + BigDecimal totalDiscountPrice = BigDecimal.ZERO; + for (Coupon coupon : couponList) { + if (coupon.getDiscountType() == DiscountType.PERCENT) { + BigDecimal discountRate = BigDecimal.valueOf(coupon.getDiscountRate()).divide(BigDecimal.valueOf(100)); + BigDecimal discountAmount = getPricePerUnit().multiply(discountRate); + totalDiscountPrice = totalDiscountPrice.add(discountAmount); + } else { + totalDiscountPrice = totalDiscountPrice.add(BigDecimal.valueOf(coupon.getDiscountPrice())); + } + } + return totalDiscountPrice.multiply(BigDecimal.valueOf(quantity)).setScale(2, RoundingMode.HALF_UP); + } + + /** + * 상품의 개당 할인 가격을 계산하여 반환하는 메소드입니다. + * + * @param coupon 쿠폰 정보 + * @return 개당 할인 가격 + */ + public BigDecimal calculateDiscountPricePerUnit(Coupon coupon) { + BigDecimal discountPricePerUnit = BigDecimal.ZERO; + + if (coupon.getDiscountType() == DiscountType.PERCENT) { + BigDecimal discountRate = BigDecimal.valueOf(coupon.getDiscountRate()).divide(BigDecimal.valueOf(100)); + discountPricePerUnit = getPricePerUnit().multiply(discountRate); + } else { + discountPricePerUnit = BigDecimal.valueOf(coupon.getDiscountPrice()); + } + + return discountPricePerUnit; + } + + + /** + * 상품에 대한 총 결제 가격을 계산하여 반환하는 메소드입니다.
+ * 총 결제 가격 = (총 주문 가격 - 할인 가격) + * @param quantity 상품 수량 + * @param couponList 쿠폰 목록 + * @return 총 결제 가격 + */ + public BigDecimal calculateTotalPaymentPrice(long quantity, List couponList) { + BigDecimal totalPrice = calculateTotalPrice(quantity); + BigDecimal totalDiscountPrice = calculateTotalDiscountPrice(couponList, quantity); + return totalPrice.subtract(totalDiscountPrice); + } } diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java index cabc3bc..a922ef1 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java @@ -2,20 +2,21 @@ import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.entity.Product; -import com.flab.offcoupon.util.DiscountUtils; import lombok.AllArgsConstructor; import lombok.Getter; +import java.math.BigDecimal; + @Getter @AllArgsConstructor public final class AppliedCouponInfo { private final long couponId; - private final long discountAmount; + private final BigDecimal discountAmount; private AppliedCouponInfo(Product product, Coupon coupon) { this.couponId = coupon.getId(); - this.discountAmount = DiscountUtils.calculateDiscountPricePerUnit(product, coupon); + this.discountAmount = product.calculateDiscountPricePerUnit(coupon); } public static AppliedCouponInfo createAppliedCouponInfo(Product product, Coupon coupon) { diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java index e149309..5200411 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java @@ -2,10 +2,10 @@ import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.entity.Product; -import com.flab.offcoupon.util.DiscountUtils; import lombok.AllArgsConstructor; import lombok.Getter; +import java.math.BigDecimal; import java.util.List; @Getter @@ -13,20 +13,20 @@ public final class OrderInfo { private final long productId; private final long quantity; - private final long pricePerEach; - private final long totalOrderPrice; + private final BigDecimal pricePerEach; + private final BigDecimal totalOrderPrice; private final List appliedCouponInfos; - private final long totalDiscountPrice; - private final long totalPaymentPrice; + private final BigDecimal totalDiscountPrice; + private final BigDecimal totalPaymentPrice; private OrderInfo(Product product, long quantity, List couponList) { this.productId = product.getId(); this.quantity = quantity; - this.pricePerEach = DiscountUtils.getProductPricePerUnit(product); - this.totalOrderPrice = DiscountUtils.calculateTotalPrice(product, quantity); + this.pricePerEach = product.getPricePerUnit(); + this.totalOrderPrice = product.calculateTotalPrice(quantity); this.appliedCouponInfos = createAppliedCouponInfos(product, couponList); - this.totalDiscountPrice = DiscountUtils.calculateTotalDiscountPrice(product, couponList, quantity); - this.totalPaymentPrice = DiscountUtils.calculateTotalPaymentPrice(product, quantity, couponList); + this.totalDiscountPrice = product.calculateTotalDiscountPrice(couponList, quantity); + this.totalPaymentPrice = product.calculateTotalPaymentPrice(quantity, couponList); } public static OrderInfo createOrderInfo(Product product, long quantity, List couponList) { diff --git a/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java index e324e30..fffcaba 100644 --- a/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java +++ b/src/main/java/com/flab/offcoupon/dto/response/AllCouponsByMemberIdResponse.java @@ -23,7 +23,7 @@ public AllCouponsByMemberIdResponse(AllCouponsByMemberIdVo vo) { this.couponId = vo.couponId(); this.category = vo.category(); this.description = vo.description(); - this.discount = DiscountUtils.getDiscount(vo.discountType(), vo.discountRate(), vo.discountPrice()); + this.discount = DiscountUtils.getDiscountInfo(vo.discountType(), vo.discountRate(), vo.discountPrice()); this.validateStartDate = vo.validateStartDate(); this.validateEndDate = vo.validateEndDate(); this.couponStatus = vo.couponStatus(); diff --git a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java index 6f175da..f22d73f 100644 --- a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java +++ b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java @@ -25,7 +25,7 @@ public AvailableCouponsByMemberIdResponse(AvailableCouponsByMemberIdVo vo) { this.couponId = vo.couponId(); this.category = vo.category(); this.description = vo.description(); - this.discount = DiscountUtils.getDiscount(vo.discountType(), vo.discountRate(), vo.discountPrice()); + this.discount = DiscountUtils.getDiscountInfo(vo.discountType(), vo.discountRate(), vo.discountPrice()); this.discountPrice = vo.discountedPrice(); this.validateStartDate = vo.validateStartDate(); this.validateEndDate = vo.validateEndDate(); diff --git a/src/main/java/com/flab/offcoupon/exception/product/ProductErrorMessage.java b/src/main/java/com/flab/offcoupon/exception/product/ProductErrorMessage.java new file mode 100644 index 0000000..eec3407 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/exception/product/ProductErrorMessage.java @@ -0,0 +1,9 @@ +package com.flab.offcoupon.exception.product; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ProductErrorMessage { + public static final String PRODUCT_NOT_EXIST = "해당 상품이 존재하지 않습니다. id: %s"; +} diff --git a/src/main/java/com/flab/offcoupon/exception/product/ProductExceptionHandler.java b/src/main/java/com/flab/offcoupon/exception/product/ProductExceptionHandler.java new file mode 100644 index 0000000..c2f31be --- /dev/null +++ b/src/main/java/com/flab/offcoupon/exception/product/ProductExceptionHandler.java @@ -0,0 +1,22 @@ +package com.flab.offcoupon.exception.product; + +import com.flab.offcoupon.util.ResponseDTO; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import static com.flab.offcoupon.exception.GlobalExceptionHandler.HTTP_REQUEST; + +@Slf4j +@RestControllerAdvice +public class ProductExceptionHandler { + @ExceptionHandler(ProductNotFoundException.class) + public ResponseEntity> couponNotFountException(ProductNotFoundException ex, HttpServletRequest request) { + log.info(HTTP_REQUEST, request.getMethod(), request.getRequestURI(), + ex.getMessage(), HttpStatus.NOT_FOUND); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ResponseDTO.getFailResult(ex.getMessage())); + } +} diff --git a/src/main/java/com/flab/offcoupon/exception/product/ProductNotFoundException.java b/src/main/java/com/flab/offcoupon/exception/product/ProductNotFoundException.java new file mode 100644 index 0000000..13a80a5 --- /dev/null +++ b/src/main/java/com/flab/offcoupon/exception/product/ProductNotFoundException.java @@ -0,0 +1,15 @@ +package com.flab.offcoupon.exception.product; + +import com.flab.offcoupon.exception.CustomException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ProductNotFoundException extends CustomException { + public ProductNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java index 1140e50..2f6b97c 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/ProductRepository.java @@ -1,10 +1,13 @@ package com.flab.offcoupon.repository.mysql; import com.flab.offcoupon.domain.entity.Product; +import com.flab.offcoupon.exception.product.ProductNotFoundException; import org.apache.ibatis.annotations.Mapper; import java.util.Optional; +import static com.flab.offcoupon.exception.product.ProductErrorMessage.PRODUCT_NOT_EXIST; + @Mapper public interface ProductRepository { /** @@ -24,7 +27,7 @@ public interface ProductRepository { default Product getProductById(long productId) { return findProductById(productId) - .orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다. id: " + productId)); + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_EXIST.formatted(productId))); } /** diff --git a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java index 2706510..07aa6d4 100644 --- a/src/main/java/com/flab/offcoupon/util/DiscountUtils.java +++ b/src/main/java/com/flab/offcoupon/util/DiscountUtils.java @@ -1,107 +1,33 @@ package com.flab.offcoupon.util; -import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.entity.DiscountType; -import com.flab.offcoupon.domain.entity.Product; import lombok.experimental.UtilityClass; -import java.util.List; - /** * 할인 정보를 반환하는 유틸리티 클래스입니다. */ @UtilityClass public class DiscountUtils { /** - * 할인 정보를 반환하는 메소드입니다.
+ * 할인 정보를 문자열로 반환하는 메소드입니다.
*

- * String 더하기 연산은 성능이 좋지 않을 수 있으므로 StringBuilder를 사용하여 최적화하였습니다. + * JDK 5이상 부터 반복문이 아닌 곳에서 String 덧셈 연산을 할 경우 + * 컴파일 최적화로 인해 해당 연산을 StringBuilder로 변환해줍니다. + *

+ * 그럼에도 불구하고 해당 메소드 내에서 StringBuilder를 사용한 이유는 + * 추후에 반복문이 사용될 가능성을 염두하여 StringBuilder를 타입으로 선택했습니다. * * @param discountType 할인 종류 (DiscountType enum 값) * @param discountRate 할인율 (percent 할인의 경우) * @param discountPrice 할인액 (amount 할인의 경우) * @return 할인 정보를 나타내는 문자열 */ - public String getDiscount(DiscountType discountType, Long discountRate, Long discountPrice) { - StringBuilder sb = new StringBuilder(); + public String getDiscountInfo(DiscountType discountType, Long discountRate, Long discountPrice) { + StringBuilder discountMsg = new StringBuilder(); if (discountType == DiscountType.AMOUNT) { - return sb.append(discountPrice).append("원 할인").toString(); - } else { - return sb.append(discountRate).append("% 할인").toString(); - } - } - - /** - * 상품의 개별 가격을 반환하는 메소드입니다.
- * 만약 세일 가격이 없다면 원래 가격을 반환합니다. - * - * @param product 상품 정보 - * @return 상품의 개별 가격 - */ - public long getProductPricePerUnit(Product product) { - return product.getSalePrice() == null ? product.getOriginalPrice() : product.getSalePrice(); - } - - /** - * 상품의 총 가격을 계산하여 반환하는 메소드입니다. - * - * @param product 상품 정보 - * @param quantity 상품 수량 - * @return 상품의 총 가격 - */ - public long calculateTotalPrice(Product product, long quantity) { - return getProductPricePerUnit(product) * quantity; - } - - /** - * 상품에 대한 총 할인 가격을 계산하여 반환하는 메소드입니다. - * - * @param product 상품 정보 - * @param couponList 쿠폰 목록 - * @param quantity 상품 수량 - * @return 총 할인 가격 - */ - public long calculateTotalDiscountPrice(Product product, List couponList, long quantity) { - long totalDiscountPrice = 0; - for (Coupon coupon : couponList) { - if (coupon.getDiscountType() == DiscountType.PERCENT) { - totalDiscountPrice += getProductPricePerUnit(product) * ((double) coupon.getDiscountRate() / 100); - } else { - totalDiscountPrice += coupon.getDiscountPrice(); - } - } - return totalDiscountPrice * quantity; - } - - /** - * 상품의 개당 할인 가격을 계산하여 반환하는 메소드입니다. - * - * @param product 상품 정보 - * @param coupon 쿠폰 정보 - * @return 개당 할인 가격 - */ - public long calculateDiscountPricePerUnit(Product product, Coupon coupon) { - long discountPricePerUnit = 0; - - if (coupon.getDiscountType() == DiscountType.PERCENT) { - discountPricePerUnit += getProductPricePerUnit(product) * coupon.getDiscountRate() / 100; + return discountMsg.append(discountPrice).append("원 할인").toString(); } else { - discountPricePerUnit += coupon.getDiscountPrice(); + return discountMsg.append(discountRate).append("% 할인").toString(); } - - return discountPricePerUnit; - } - - - /** - * 상품에 대한 총 결제 가격을 계산하여 반환하는 메소드입니다. - * - * @param product 상품 정보 - * @param quantity 상품 수량 - * @param couponList 쿠폰 목록 - * @return 총 결제 가격 - */ - public long calculateTotalPaymentPrice(Product product, long quantity, List couponList) { - return calculateTotalPrice(product, quantity) - calculateTotalDiscountPrice(product, couponList, quantity); } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 800cd34..a0ee965 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -8,28 +8,28 @@ DROP TABLE IF EXISTS order_coupon; CREATE TABLE member ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '회원 식별자', - email VARCHAR(255) NOT NULL COMMENT '회원 이메일', - password VARCHAR(255) NOT NULL COMMENT '비밀번호', - name VARCHAR(100) NOT NULL COMMENT '회원 이름', - birthdate DATE NOT NULL COMMENT '생년월일', - phone VARCHAR(50) NOT NULL COMMENT '휴대폰 번호', - role VARCHAR(50) NOT NULL COMMENT '회원 권한', - created_at DATETIME NOT NULL COMMENT '데이터 생성일', - updated_at DATETIME NOT NULL COMMENT '데이터 변경일' + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '회원 식별자', + email VARCHAR(255) NOT NULL COMMENT '회원 이메일', + password VARCHAR(255) NOT NULL COMMENT '비밀번호', + name VARCHAR(100) NOT NULL COMMENT '회원 이름', + birthdate DATE NOT NULL COMMENT '생년월일', + phone VARCHAR(50) NOT NULL COMMENT '휴대폰 번호', + role VARCHAR(50) NOT NULL COMMENT '회원 권한', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); CREATE TABLE event ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '이벤트 식별자', + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '이벤트 식별자', category VARCHAR(100) NOT NULL COMMENT '상품 카테고리', description VARCHAR(255) NOT NULL COMMENT '이벤트 설명', start_date DATE NULL COMMENT '이벤트 시작일 / null일 경우 무제한 이벤트', end_date DATE NULL COMMENT '이벤트 종료일 / null일 경우 무제한 이벤트', daily_issue_start_time VARCHAR(20) NULL COMMENT '당일 쿠폰 발행 시작시간 e.g. "13:00:00"/ null일 경우 무한 발행', daily_issue_end_time VARCHAR(20) NULL COMMENT '당일 쿠폰 발행 종료시간 e.g. "15:00:00" / null일 경우 무한 발행', - created_at DATETIME NOT NULL COMMENT '데이터 생성일', - updated_at DATETIME NOT NULL COMMENT '데이터 변경일' + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); CREATE TABLE coupon @@ -39,7 +39,7 @@ CREATE TABLE coupon discount_type VARCHAR(50) NOT NULL COMMENT '정액, 정률 등', discount_rate BIGINT UNSIGNED NULL COMMENT '정률 할인', discount_price BIGINT UNSIGNED NULL COMMENT '정액 할인', - coupon_type VARCHAR(100) NOT NULL COMMENT '선착순 쿠폰, 회원가입 쿠폰 등..', + coupon_type VARCHAR(100) NOT NULL COMMENT '선착순 쿠폰, 회원가입 쿠폰 등..', max_quantity BIGINT UNSIGNED NULL COMMENT '무제한 발행일 경우 NULL', issued_quantity BIGINT UNSIGNED NULL COMMENT '무제한 발행일 경우 NULL', validate_start_date DATETIME NOT NULL COMMENT '모든 쿠폰은 유효 시간이 있어야한다는 제약 사항 존재', @@ -50,48 +50,49 @@ CREATE TABLE coupon CREATE TABLE coupon_issue ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰 발행 기록', - member_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', - coupon_id BIGINT UNSIGNED NOT NULL COMMENT '회원 ID', - coupon_status VARCHAR(255) NOT NULL DEFAULT 'NOT_ACTIVE' COMMENT '유효일 전 : NOT_ACTIVE / 유효기간 : ACTIVE / 사용완료 : USED / 만료 : EXPIRED', - created_at DATETIME NOT NULL COMMENT '데이터 생성일', - updated_at DATETIME NOT NULL COMMENT '데이터 변경일', - check_related_issued_quantity BOOLEAN DEFAULT FALSE COMMENT '쿠폰 발행시 발행량 체크 여부' + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰 발행 기록', + member_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', + coupon_id BIGINT UNSIGNED NOT NULL COMMENT '회원 ID', + coupon_status VARCHAR(255) NOT NULL DEFAULT 'NOT_ACTIVE' COMMENT '유효일 전 : NOT_ACTIVE / 유효기간 : ACTIVE / 사용완료 : USED / 만료 : EXPIRED', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일', + check_related_issued_quantity BOOLEAN DEFAULT FALSE COMMENT '쿠폰 발행시 발행량 체크 여부' ); CREATE TABLE product ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '상품 식별자', - category VARCHAR(255) NOT NULL COMMENT '상품 카테고리', - title VARCHAR(255) NOT NULL COMMENT '상품명', - description VARCHAR(255) NOT NULL COMMENT '상품 설명', - original_price BIGINT UNSIGNED NOT NULL COMMENT '원래 상품 가격', - sale_price BIGINT UNSIGNED NULL COMMENT '할인 상품 가격(쿠폰과 관계없이 전체적으로 할인할 경우)', - min_order_price BIGINT UNSIGNED NULL COMMENT '최소 주문 가격', - created_at DATETIME NOT NULL COMMENT '데이터 생성일', - updated_at DATETIME NOT NULL COMMENT '데이터 변경일' + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '상품 식별자', + category VARCHAR(255) NOT NULL COMMENT '상품 카테고리', + title VARCHAR(255) NOT NULL COMMENT '상품명', + description VARCHAR(255) NOT NULL COMMENT '상품 설명', + original_price DECIMAL(12, 2) UNSIGNED NOT NULL COMMENT '원래 상품 가격', + sale_price DECIMAL(12, 2) UNSIGNED DEFAULT 0 NOT NULL COMMENT '할인 상품 가격(쿠폰과 관계없이 전체적으로 할인할 경우)', + min_order_price DECIMAL(12, 2) UNSIGNED NULL COMMENT '최소 주문 가격', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); CREATE TABLE order_detail ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '주문 식별자', - product_id BIGINT UNSIGNED NOT NULL COMMENT '상품 ID', - quantity BIGINT UNSIGNED NOT NULL COMMENT '상품 주문 수량', - price_per_each BIGINT UNSIGNED NOT NULL COMMENT '상품 개당 가격', - total_order_price BIGINT UNSIGNED NOT NULL COMMENT '총 상품 주문 가격', - total_discount_price BIGINT UNSIGNED NOT NULL COMMENT '총 할인 가격', - total_payment_price BIGINT UNSIGNED NOT NULL COMMENT '총 결제 가격', - created_at DATETIME NOT NULL COMMENT '데이터 생성일', - updated_at DATETIME NOT NULL COMMENT '데이터 변경일' + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '주문 식별자', + product_id BIGINT UNSIGNED NOT NULL COMMENT '상품 ID', + quantity BIGINT UNSIGNED NOT NULL COMMENT '상품 주문 수량', + price_per_each DECIMAL(12, 2) UNSIGNED NOT NULL COMMENT '상품 개당 가격', + total_order_price DECIMAL(12, 2) UNSIGNED NOT NULL COMMENT '총 상품 주문 가격', + total_discount_price DECIMAL(12, 2) UNSIGNED NOT NULL COMMENT '총 할인 가격', + total_payment_price DECIMAL(12, 2) UNSIGNED NOT NULL COMMENT '총 결제 가격', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); ## 주문 한 개에 여러 쿠폰을 사용할 수 있으므로 별도의 테이블로 분리 -CREATE TABLE order_coupon ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰을 사용한 주문 식별자', - order_id BIGINT UNSIGNED NOT NULL COMMENT '주문 ID', - coupon_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', - discount_amount BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 할인액', - created_at DATETIME NOT NULL COMMENT '데이터 생성일', - updated_at DATETIME NOT NULL COMMENT '데이터 변경일' +CREATE TABLE order_coupon +( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '쿠폰을 사용한 주문 식별자', + order_id BIGINT UNSIGNED NOT NULL COMMENT '주문 ID', + coupon_id BIGINT UNSIGNED NOT NULL COMMENT '쿠폰 ID', + discount_amount DECIMAL(12, 2) UNSIGNED NOT NULL COMMENT '쿠폰 할인액', + created_at DATETIME NOT NULL COMMENT '데이터 생성일', + updated_at DATETIME NOT NULL COMMENT '데이터 변경일' ); From 4e3d989f33327070c735e78424f0574721517ee6 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 14:08:21 +0900 Subject: [PATCH 22/27] =?UTF-8?q?refactor=20:=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20=EC=BF=A0=ED=8F=B0=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=96=88?= =?UTF-8?q?=EB=8B=A4=EB=8A=94=20=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flab/offcoupon/repository/mysql/CouponIssueRepository.java | 2 +- .../com/flab/offcoupon/service/coupon_use/OrderService.java | 2 +- src/main/resources/mapper/CouponIssueMapper.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java index e6f8488..fcddda0 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java @@ -65,5 +65,5 @@ public interface CouponIssueRepository { * 쿠폰 상태를 사용 완료로 변경 * @param couponIssueIds 쿠폰 발급 ID 목록 */ - void updateCouponStatus(List couponIssueIds); + void useCoupon(List couponIssueIds); } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 61f3181..ac55ee8 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -101,7 +101,7 @@ public ResponseDTO orderProduct(final long productId, final OrderProduct } // 5. 쿠폰 사용 처리 - couponIssueRepository.updateCouponStatus(request.getCouponIssueId()); + couponIssueRepository.useCoupon(request.getCouponIssueId()); return ResponseDTO.getSuccessResult("쿠폰 처리 및 주문이 완료되었습니다."); } diff --git a/src/main/resources/mapper/CouponIssueMapper.xml b/src/main/resources/mapper/CouponIssueMapper.xml index 702d905..8f864b4 100644 --- a/src/main/resources/mapper/CouponIssueMapper.xml +++ b/src/main/resources/mapper/CouponIssueMapper.xml @@ -114,7 +114,7 @@ ); - + UPDATE coupon_issue SET coupon_status = 'USED' WHERE id in From 784033ca713f59e7320ca63789691577de5f850a Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 19:00:06 +0900 Subject: [PATCH 23/27] =?UTF-8?q?refactor=20:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=9C=20?= =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=ED=9B=84=20=EC=95=A0=ED=94=8C=EB=A6=AC=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EA=B0=80=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offcoupon/domain/entity/OrderCoupon.java | 2 +- .../offcoupon/domain/entity/OrderDetail.java | 2 +- .../{params => helper}/AppliedCouponInfo.java | 6 +- .../entity/helper/AvailableCouponInfo.java | 108 ++++++++++++++++++ .../entity/{params => helper}/OrderInfo.java | 6 +- .../order/AvailableCouponsByMemberIdVo.java | 25 ++-- .../AvailableCouponsByMemberIdResponse.java | 30 ++--- .../mysql/CouponIssueRepository.java | 7 +- .../service/coupon_use/OrderService.java | 95 ++++++++++----- src/main/resources/data.sql | 2 +- .../resources/mapper/CouponIssueMapper.xml | 21 +--- 11 files changed, 228 insertions(+), 76 deletions(-) rename src/main/java/com/flab/offcoupon/domain/entity/{params => helper}/AppliedCouponInfo.java (77%) create mode 100644 src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java rename src/main/java/com/flab/offcoupon/domain/entity/{params => helper}/OrderInfo.java (89%) diff --git a/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java b/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java index 483f78a..0813b3d 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/OrderCoupon.java @@ -1,6 +1,6 @@ package com.flab.offcoupon.domain.entity; -import com.flab.offcoupon.domain.entity.params.AppliedCouponInfo; +import com.flab.offcoupon.domain.entity.helper.AppliedCouponInfo; import com.flab.offcoupon.domain.entity.params.TimeParams; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java b/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java index a3c3c54..d19eb21 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/OrderDetail.java @@ -1,6 +1,6 @@ package com.flab.offcoupon.domain.entity; -import com.flab.offcoupon.domain.entity.params.OrderInfo; +import com.flab.offcoupon.domain.entity.helper.OrderInfo; import com.flab.offcoupon.domain.entity.params.TimeParams; import com.flab.offcoupon.util.DateTimeUtils; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/helper/AppliedCouponInfo.java similarity index 77% rename from src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java rename to src/main/java/com/flab/offcoupon/domain/entity/helper/AppliedCouponInfo.java index a922ef1..40314f8 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/params/AppliedCouponInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/helper/AppliedCouponInfo.java @@ -1,4 +1,4 @@ -package com.flab.offcoupon.domain.entity.params; +package com.flab.offcoupon.domain.entity.helper; import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.entity.Product; @@ -7,6 +7,10 @@ import java.math.BigDecimal; +/** + * 주문에 적용된 쿠폰 정보를 담는 클래스입니다. + * 비즈니스 로직에서 데이터 가공을 위해 사용됩니다. + */ @Getter @AllArgsConstructor public final class AppliedCouponInfo { diff --git a/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java new file mode 100644 index 0000000..8b61d9b --- /dev/null +++ b/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java @@ -0,0 +1,108 @@ +package com.flab.offcoupon.domain.entity.helper; + +import com.flab.offcoupon.domain.entity.CouponStatus; +import com.flab.offcoupon.domain.entity.DiscountType; +import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; +import com.flab.offcoupon.util.DiscountUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; + +/** + * 사용 가능한 쿠폰 정보를 담는 클래스입니다. + * 비즈니스 로직에서 데이터 가공을 위해 사용됩니다. + */ +@ToString +@Getter +@AllArgsConstructor +public final class AvailableCouponInfo { + private final long couponId; + private final String category; + private final String description; + private final BigDecimal minProductPrice; // 최소 주문 금액 + private final BigDecimal productPrice; // 상품 가격 + private final String discount; // 할인 쿠폰 내용 + private final BigDecimal discountPrice; // 상품 가격에 할인 쿠폰 적용한 할인가 + private final LocalDateTime validateStartDate; + private final LocalDateTime validateEndDate; + private final long couponIssueId; + private final CouponStatus couponStatus; + + public AvailableCouponInfo(AvailableCouponsByMemberIdVo vo) { + this.couponId = vo.couponId(); + this.category = vo.category(); + this.description = vo.description(); + this.minProductPrice = vo.minOrderPrice(); + this.productPrice = productPrice(vo); + this.discount = DiscountUtils.getDiscountInfo(vo.discountType(), vo.discountRate(), vo.discountPrice()); + this.discountPrice = calculateDiscountPrice(vo); + this.validateStartDate = vo.validateStartDate(); + this.validateEndDate = vo.validateEndDate(); + this.couponIssueId = vo.couponIssueId(); + this.couponStatus = vo.couponStatus(); + } + + /** + * 상품의 salePrice가 0보다 큰지 확인합니다. + * + * @param salePrice 상품의 할인 가격 + * @return salePrice가 0보다 크면 true, 아니면 false + */ + private boolean isSalePriceOverThanZero(BigDecimal salePrice) { + return salePrice.compareTo(BigDecimal.ZERO) > 0; + } + + private BigDecimal productPrice(AvailableCouponsByMemberIdVo vo) { + if (isSalePriceOverThanZero(vo.salePrice())) { + return vo.salePrice(); + } + return vo.originalPrice(); + } + + /** + * 할인 가격을 계산합니다.
+ * 상품 가격은 salePrice가 0보다 크면 salePrice를, salePrice가 0보다 작으면 originalPrice를 사용합니다. + * 할인 가격은 할인 타입에 따라 다르게 계산됩니다.
+ *
  • 할인 타입이 AMOUNT일 경우, 할인 가격은 discountPrice를 사용합니다.
  • + *
  • 할인 타입이 PERCENT일 경우 할인 가격은 price * discountRate / 100을 사용합니다.
  • + * + * @param vo 사용 가능한 쿠폰 정보 + * @return 할인 가격 + */ + public BigDecimal calculateDiscountPrice(AvailableCouponsByMemberIdVo vo) { + long discountRate = vo.discountRate() == null ? 0 : vo.discountRate(); + long discountPrice = vo.discountPrice() == null ? 0 : vo.discountPrice(); + if (isSalePriceOverThanZero(vo.salePrice())) { + return calculateDiscountPrice(vo.discountType(), vo.salePrice(), discountRate, discountPrice); + } else { + return calculateDiscountPrice(vo.discountType(), vo.originalPrice(), discountRate, discountPrice); + } + } + + /** + * 할인 가격을 계산합니다.
    + * + * @param discountType 할인 타입 + * @param price 상품 가격 + * @param discountRate 할인율 + * @param discountedPrice 할인액 + * @return + */ + public BigDecimal calculateDiscountPrice(DiscountType discountType, BigDecimal price, long discountRate, long discountedPrice) { + if (isDiscountTypePercent(discountType)) { + // 할인율을 백분율로 변환하여 가격에서 할인 가격을 계산 + BigDecimal discountPercentage = BigDecimal.valueOf(discountRate).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + return price.multiply(discountPercentage).setScale(0, RoundingMode.DOWN); // 소수점 이하 자릿수를 제거하여 반환 + } else { + return BigDecimal.valueOf(discountedPrice); + } + } + + private boolean isDiscountTypePercent(DiscountType discountType) { + return discountType == DiscountType.PERCENT; + } +} diff --git a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/helper/OrderInfo.java similarity index 89% rename from src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java rename to src/main/java/com/flab/offcoupon/domain/entity/helper/OrderInfo.java index 5200411..2de4835 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/params/OrderInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/helper/OrderInfo.java @@ -1,4 +1,4 @@ -package com.flab.offcoupon.domain.entity.params; +package com.flab.offcoupon.domain.entity.helper; import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.entity.Product; @@ -8,6 +8,10 @@ import java.math.BigDecimal; import java.util.List; +/** + * 주문 정보를 담는 클래스입니다. + * 비즈니스 로직에서 데이터 가공을 위해 사용됩니다. + */ @Getter @AllArgsConstructor public final class OrderInfo { diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java index e63151f..a6b9d73 100644 --- a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java +++ b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/AvailableCouponsByMemberIdVo.java @@ -3,6 +3,7 @@ import com.flab.offcoupon.domain.entity.CouponStatus; import com.flab.offcoupon.domain.entity.DiscountType; +import java.math.BigDecimal; import java.time.LocalDateTime; /** @@ -10,16 +11,18 @@ * 마이페이지에서 현재 회원이 가진 모든 쿠폰을 조회하기 위해 사용 */ public record AvailableCouponsByMemberIdVo ( - long couponId, - LocalDateTime validateStartDate, - LocalDateTime validateEndDate, - DiscountType discountType, - Long discountRate,// NULL 일 경우 AMOUNT - Long discountPrice,// NULL 일 경우 PERCENT - String category, - String description, - long couponIssueId, - CouponStatus couponStatus, - long discountedPrice // 할인 가격 + long couponId, + LocalDateTime validateStartDate, + LocalDateTime validateEndDate, + DiscountType discountType, + Long discountRate,// NULL 일 경우 AMOUNT + Long discountPrice,// NULL 일 경우 PERCENT + String category, + String description, + BigDecimal minOrderPrice, + long couponIssueId, + CouponStatus couponStatus, + BigDecimal originalPrice, // 원래 가격 + BigDecimal salePrice // 할인 가격(쿠폰 적용과 상관없음) ) {} diff --git a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java index f22d73f..d2b0483 100644 --- a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java +++ b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java @@ -1,11 +1,11 @@ package com.flab.offcoupon.dto.response; import com.flab.offcoupon.domain.entity.CouponStatus; -import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; -import com.flab.offcoupon.util.DiscountUtils; +import com.flab.offcoupon.domain.entity.helper.AvailableCouponInfo; import lombok.AllArgsConstructor; import lombok.Getter; +import java.math.BigDecimal; import java.time.LocalDateTime; @Getter @@ -14,22 +14,24 @@ public final class AvailableCouponsByMemberIdResponse { private final long couponId; private final String category; private final String description; - private final String discount; - private final long discountPrice; + private final String discount; // 할인 쿠폰 내용 + private final BigDecimal discountPrice; // 상품 가격에 할인 쿠폰 적용한 할인가 private final LocalDateTime validateStartDate; private final LocalDateTime validateEndDate; private final long couponIssueId; private final CouponStatus couponStatus; - public AvailableCouponsByMemberIdResponse(AvailableCouponsByMemberIdVo vo) { - this.couponId = vo.couponId(); - this.category = vo.category(); - this.description = vo.description(); - this.discount = DiscountUtils.getDiscountInfo(vo.discountType(), vo.discountRate(), vo.discountPrice()); - this.discountPrice = vo.discountedPrice(); - this.validateStartDate = vo.validateStartDate(); - this.validateEndDate = vo.validateEndDate(); - this.couponIssueId = vo.couponIssueId(); - this.couponStatus = vo.couponStatus(); + + public AvailableCouponsByMemberIdResponse(AvailableCouponInfo info) { + this.couponId = info.getCouponId(); + this.category = info.getCategory(); + this.description = info.getDescription(); + this.discount = info.getDiscount(); + this.discountPrice = info.getDiscountPrice(); + this.validateStartDate = info.getValidateStartDate(); + this.validateEndDate = info.getValidateEndDate(); + this.couponIssueId = info.getCouponIssueId(); + this.couponStatus = info.getCouponStatus(); } + } diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java index fcddda0..49d407c 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/CouponIssueRepository.java @@ -37,13 +37,12 @@ public interface CouponIssueRepository { List getAllCoupons(long memberId); /** - * 물건 구매 시 사용 가능한 쿠폰 조회
    + * 상품 주문 시 사용 가능한 쿠폰 조회
    *
      *
    • 사용 가능한 쿠폰 목록 조회 조건
    • *
        - *
      1. 할인율이 적용된 가격(discounted_price) 기준으로 내림차순 정렬
      2. - *
      3. discounted_price는 쿠폰의 할인율 또는 할인액에 따라 원래 상품 가격 혹은 할인 상품 가격에 계산
      4. - *
      5. 쿠폰의 상태가 활성화인 상태이고, 현재 날짜 기준으로 유효 기간 범위 내에 있는 쿠폰 조회
      6. + *
      7. product_id와 member_id가 가 매개변수로 받은 ID식별자인 경우
      8. + *
      9. 쿠폰의 상태가 활성화인 상태이고, 현재 날짜 기준으로 유효 기간 범위 내에 있는 경우/li> *
      *
    * diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index ac55ee8..733c8f6 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -1,8 +1,9 @@ package com.flab.offcoupon.service.coupon_use; import com.flab.offcoupon.domain.entity.OrderDetail; -import com.flab.offcoupon.domain.entity.params.AppliedCouponInfo; -import com.flab.offcoupon.domain.entity.params.OrderInfo; +import com.flab.offcoupon.domain.entity.helper.AppliedCouponInfo; +import com.flab.offcoupon.domain.entity.helper.AvailableCouponInfo; +import com.flab.offcoupon.domain.entity.helper.OrderInfo; import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; import com.flab.offcoupon.domain.vo.persistence.order.CouponIssuesAreActiveVo; import com.flab.offcoupon.domain.vo.persistence.order.MemberIdProductIdNowVo; @@ -17,13 +18,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import static com.flab.offcoupon.domain.entity.OrderCoupon.createOrderCoupon; import static com.flab.offcoupon.domain.entity.OrderDetail.createOrderDetail; -import static com.flab.offcoupon.domain.entity.params.OrderInfo.createOrderInfo; +import static com.flab.offcoupon.domain.entity.helper.OrderInfo.createOrderInfo; import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_IS_NOT_ACTIVE; import static com.flab.offcoupon.exception.coupon.CouponErrorMessage.COUPON_USAGE_INVALID_PERIOD; @@ -39,41 +42,78 @@ public class OrderService { /** * 사용 가능한 쿠폰 목록 조회 - * @param memberId 회원 ID + *

    + * Query에서 로직 사용을 최소화하는것이 목표입니다.
    + * Query는 디버깅이 어려우며 수정이 필요할때 유연하게 대응하기 어렵고, 테스트 코드를 작성하기도 어렵습니다.
    + * 따라서 데이터 조회 쿼리를 심플하게 만들고 모든 가공은 애플리케이션 내에서 하도록 진행했습니다.
    + *

    + *

    + * 아래는 쿼리에서 로직 사용을 피하기 위한 경우입니다. + *

      + *
    1. query에서 분기를 태우는 case-when-then
    2. + *
    3. query에서 값을 계산하는 경우
    4. + *
    5. query에서 비즈니스 로직이 있는 경우
    6. + *
    + *

    + * + * @param memberId 회원 ID * @param productId 상품 ID - * @param now 현재 날짜 + * @param now 현재 날짜 * @return 사용 가능한 쿠폰 목록 */ @Transactional(readOnly = true) public ResponseDTO> getAvailableCoupons(final long memberId, final long productId, final LocalDateTime now) { - List availableCoupons = + List availableCouponData = couponIssueRepository.getAvailableCoupons(new MemberIdProductIdNowVo(memberId, productId, now)); - List responseList = new ArrayList<>(); - filterAvailableCouponsWithMinPrice(productId, availableCoupons, responseList); - return ResponseDTO.getSuccessResult(responseList); + + // 할인가격 기준으로 내림차순 + List availableCouponInfos = availableCouponData.stream() + .map(AvailableCouponInfo::new) + .sorted(Comparator.comparing(AvailableCouponInfo::getDiscountPrice).reversed()) + .toList(); + + return ResponseDTO.getSuccessResult(filterAvailableCoupons(availableCouponInfos)); } /** - * 최소 주문 금액과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. - * - * @param productId 상품 ID - * @param availableCoupons 사용 가능한 쿠폰 목록 - * @param responseList 결과 리스트 + * 사용 가능한 쿠폰 목록을 필터링합니다. + * 필터링 조건은 아래와 같습니다. + *
      + *
    1. 내림차순으로 정렬된 할인 가격을 하나씩 꺼내어 누적합니다.
    2. + *
    3. 누적된 할인가격을 반영한 가격이 최소 주문 금액 이상이라면 리스트에 추가합니다.
    4. + *
    5. 현재 할인 가격이 2번 조건에서 일치하지 않을 경우 누적 할인 가격에서 제외합니다.
    6. + *
    + * @param availableCouponInfos + * @return */ - private void filterAvailableCouponsWithMinPrice(long productId, List availableCoupons, List responseList) { - // min_price(상품의 최소 주문 금액)과 비교하여 조건에 맞는 쿠폰만 결과 리스트에 추가합니다. - if (!availableCoupons.isEmpty()) { - long accumulatedDiscountPrice = 0; - for (AvailableCouponsByMemberIdVo availableCoupon : availableCoupons) { - // 최소 주문 금액까지 할인 가능한 금액 누적 - accumulatedDiscountPrice += availableCoupon.discountedPrice(); - // 할인 금액이 min_price보다 작거나 같으면 결과 리스트에 추가 - if (accumulatedDiscountPrice <= productRepository.getProductMinOrderPriceById(productId)) { - AvailableCouponsByMemberIdResponse response = new AvailableCouponsByMemberIdResponse(availableCoupon); - responseList.add(response); - } + private List filterAvailableCoupons(List availableCouponInfos) { + // 할인 가격을 누적하되 minOrderPrice를 초과하지 않는 누적 금액에 포함되는 경우만 결과 리스트에 추가 (중복 쿠폰 할인 가능용) + BigDecimal accumulatedDiscountPrice = BigDecimal.ZERO; + List responseList = new ArrayList<>(); + for (AvailableCouponInfo info : availableCouponInfos) { + BigDecimal discountPrice = info.getDiscountPrice(); + accumulatedDiscountPrice = accumulatedDiscountPrice.add(discountPrice); + if (isOverThanMinOrderPrice(info.getProductPrice(), info.getMinProductPrice(), accumulatedDiscountPrice)) { + responseList.add(new AvailableCouponsByMemberIdResponse(info)); + } else { + accumulatedDiscountPrice = accumulatedDiscountPrice.subtract(discountPrice); } } + return responseList; + } + + /** + * 최소 주문 금액과 비교하여 조건에 맞는 쿠폰인지 확인합니다. + * + * @param price 상품 가격 + * @param minOrderPrice 최소 주문 금액 + * @param accumulatedDiscountPrice 누적된 할인 가격 + * @return 할인이 적용된 금액이 최소 주문 금액보다 크거나 같으면 true, 아니면 false + */ + private boolean isOverThanMinOrderPrice(BigDecimal price, BigDecimal minOrderPrice, BigDecimal accumulatedDiscountPrice) { + // 쿠폰 할인을 적용한 가격을 구함 + BigDecimal appliedDiscount = price.subtract(accumulatedDiscountPrice); + return appliedDiscount.compareTo(minOrderPrice) >= 0; } /** @@ -84,7 +124,8 @@ private void filterAvailableCouponsWithMinPrice(long productId, List orderProduct(final long productId, final OrderProductRequest request, LocalDateTime now) { + public ResponseDTO orderProduct(final long productId, final OrderProductRequest request, LocalDateTime + now) { validateCouponIsAvailable(request, now); // 3. 주문 정보 저장 OrderInfo orderInfo = createOrderInfo( diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index e9f5f5e..2f626e3 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -16,5 +16,5 @@ VALUES (1, '바디케어' , '바디케어 전품목 이벤트', now(),DATE_ADD(n ## PRODUCT INSERT INTO product (category, title, description, original_price, sale_price, created_at, updated_at) VALUES - ('의류', '면티셔츠', '편안한 면소재의 티셔츠', 20000, NULL, NOW(), NOW()), + ('의류', '면티셔츠', '편안한 면소재의 티셔츠', 20000, 0, NOW(), NOW()), ('전자제품', '스마트폰', '고성능 스마트폰', 1000000, 800000, NOW(), NOW()); diff --git a/src/main/resources/mapper/CouponIssueMapper.xml b/src/main/resources/mapper/CouponIssueMapper.xml index 8f864b4..4111761 100644 --- a/src/main/resources/mapper/CouponIssueMapper.xml +++ b/src/main/resources/mapper/CouponIssueMapper.xml @@ -78,29 +78,20 @@ c.discount_rate, c.discount_price, p.category, - e.description, + p.description, + p.min_order_price, ci.id as couponIssueId, ci.coupon_status, - ROUND(CASE - WHEN p.sale_price IS NULL THEN - p.original_price - - IF(c.discount_type = 'PERCENT', p.original_price * (c.discount_rate / 100), - c.discount_price) - ELSE - p.sale_price - IF(c.discount_type = 'PERCENT', p.sale_price * (c.discount_rate / 100), - c.discount_price) - END, 0) - AS discounted_price + p.original_price, + p.sale_price FROM coupon_issue ci JOIN coupon c ON c.id = ci.coupon_id JOIN event e ON e.id = c.event_id JOIN product p ON p.category = e.category WHERE ci.member_id = #{memberId} - AND c.validate_start_date <= #{currentDateTime} - AND c.validate_end_date >= #{currentDateTime} + AND #{currentDateTime} BETWEEN c.validate_start_date and c.validate_end_date AND ci.coupon_status = 'ACTIVE' - AND p.id = #{productId} - ORDER BY discounted_price; + AND p.id = #{productId}; ]]> From d255cd065a3f585d0859bca1931c16b8a3391071 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 19:55:06 +0900 Subject: [PATCH 24/27] =?UTF-8?q?refactor=20:=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A1=9C=EC=A7=81=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...dVo.java => CouponValidationPeriodVo.java} | 7 +++-- .../repository/mysql/CouponRepository.java | 9 ++---- .../service/coupon_use/OrderService.java | 31 +++++++++++++------ src/main/resources/mapper/CouponMapper.xml | 14 ++++----- 4 files changed, 35 insertions(+), 26 deletions(-) rename src/main/java/com/flab/offcoupon/domain/vo/persistence/order/{ValidateNowIsBetweenPeriodVo.java => CouponValidationPeriodVo.java} (61%) diff --git a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateNowIsBetweenPeriodVo.java b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/CouponValidationPeriodVo.java similarity index 61% rename from src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateNowIsBetweenPeriodVo.java rename to src/main/java/com/flab/offcoupon/domain/vo/persistence/order/CouponValidationPeriodVo.java index 7cd356a..0caf3fe 100644 --- a/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/ValidateNowIsBetweenPeriodVo.java +++ b/src/main/java/com/flab/offcoupon/domain/vo/persistence/order/CouponValidationPeriodVo.java @@ -4,12 +4,13 @@ /** * MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO + * * @param couponId 쿠폰 ID - * @param isBetweenValidatePeriod 현재 시간이 쿠폰의 유효기간 범위내에 있는지 여부 + * @Param validateStartDate 쿠폰 유효기간 시작일 + * @Param validateEndDate 쿠폰 유효기간 종료일 */ -public record ValidateNowIsBetweenPeriodVo( +public record CouponValidationPeriodVo( long couponId, - boolean isBetweenValidatePeriod, LocalDateTime validateStartDate, LocalDateTime validateEndDate ) { diff --git a/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java b/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java index f5846ae..c53ddfe 100644 --- a/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java +++ b/src/main/java/com/flab/offcoupon/repository/mysql/CouponRepository.java @@ -2,12 +2,11 @@ import com.flab.offcoupon.domain.entity.Coupon; import com.flab.offcoupon.domain.vo.persistence.couponissue.UpdateTotalIssuedQuantityVo; -import com.flab.offcoupon.domain.vo.persistence.order.ValidateNowIsBetweenPeriodVo; +import com.flab.offcoupon.domain.vo.persistence.order.CouponValidationPeriodVo; import com.flab.offcoupon.exception.coupon.CouponNotFoundException; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -60,12 +59,10 @@ default Coupon getCouponById(long couponId) { void updateTotalIssuedCouponQuantity(UpdateTotalIssuedQuantityVo updateTotalIssuedQuantityVo); /** - * 현재 시간이 쿠폰의 유효기간 범위내에 있는지 확인 + * 쿠폰의 유효시간 범위 조회 * * @param couponIds 쿠폰 ID 리스트 - * @param currentDateTime 현재 시간 * @return 현재 시간이 쿠폰의 유효기간 범위내에 있는지 여부 */ - List validateNowIsBetweenPeriod(@Param("couponIds") List couponIds, - @Param("currentDateTime") LocalDateTime currentDateTime); + List getCouponValidationPeriod(@Param("couponIds") List couponIds); } diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 733c8f6..ba56b94 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -6,8 +6,8 @@ import com.flab.offcoupon.domain.entity.helper.OrderInfo; import com.flab.offcoupon.domain.vo.persistence.order.AvailableCouponsByMemberIdVo; import com.flab.offcoupon.domain.vo.persistence.order.CouponIssuesAreActiveVo; +import com.flab.offcoupon.domain.vo.persistence.order.CouponValidationPeriodVo; import com.flab.offcoupon.domain.vo.persistence.order.MemberIdProductIdNowVo; -import com.flab.offcoupon.domain.vo.persistence.order.ValidateNowIsBetweenPeriodVo; import com.flab.offcoupon.dto.request.OrderProductRequest; import com.flab.offcoupon.dto.response.AvailableCouponsByMemberIdResponse; import com.flab.offcoupon.exception.coupon.CouponStatusException; @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; @@ -155,19 +156,31 @@ public ResponseDTO orderProduct(final long productId, final OrderProduct private void validateCouponIsAvailable(OrderProductRequest request, LocalDateTime now) { // 1. 쿠폰들의 상태가 ACTIVE인지 확인 List couponIssueStatus = couponIssueRepository.validateStatusIsActive(request.getCouponIssueId()); - for (CouponIssuesAreActiveVo couponIssue : couponIssueStatus) { - if (!couponIssue.isActive()) { - throw new CouponStatusException(COUPON_IS_NOT_ACTIVE.formatted(couponIssue.couponIssueId())); - } + if (couponIssueStatus.stream().anyMatch(couponIssue -> !couponIssue.isActive())) { + throw new CouponStatusException(COUPON_IS_NOT_ACTIVE.formatted(couponIssueStatus.get(0).couponIssueId())); } // 2. 현재 시간이 쿠폰의 유효기간 범위내에 있는지 확인 - List isBetweenValidatePeriodVo = couponRepository.validateNowIsBetweenPeriod(request.getCouponId(), now); - for (ValidateNowIsBetweenPeriodVo isBetweenValidatePeriod : isBetweenValidatePeriodVo) { - if (!isBetweenValidatePeriod.isBetweenValidatePeriod()) { + List isBetweenValidatePeriodVo = couponRepository.getCouponValidationPeriod(request.getCouponId()); + for (CouponValidationPeriodVo validationPeriod : isBetweenValidatePeriodVo) { + if (!isNowBetweenValidatePeriod(now, validationPeriod)) { throw new CouponUsageInvalidPeriodException(COUPON_USAGE_INVALID_PERIOD - .formatted(isBetweenValidatePeriod.couponId(), isBetweenValidatePeriod.validateStartDate(), isBetweenValidatePeriod.validateEndDate())); + .formatted(validationPeriod.couponId(), validationPeriod.validateStartDate(), validationPeriod.validateEndDate())); } } } + private boolean isNowBetweenValidatePeriod(LocalDateTime now, CouponValidationPeriodVo validationPeriod) { + if(validationPeriod.validateStartDate() == null || validationPeriod.validateEndDate() == null) { + return false; + } + /** + * 현재 날짜가 유효 기간 범위 내에 있는지 확인 + * Duration.between(a,b) : a와 b 사이의 시간을 반환, a가 b보다 시간상으로 이전이면 양수, 이후면 음수 + */ + Duration startDuration = Duration.between(validationPeriod.validateStartDate(), now); + Duration endDuration = Duration.between(now, validationPeriod.validateEndDate()); + + return (startDuration.isZero() || !startDuration.isNegative()) && + (endDuration.isZero() || !endDuration.isNegative()); + } } diff --git a/src/main/resources/mapper/CouponMapper.xml b/src/main/resources/mapper/CouponMapper.xml index 9ce97e6..122cbed 100644 --- a/src/main/resources/mapper/CouponMapper.xml +++ b/src/main/resources/mapper/CouponMapper.xml @@ -81,20 +81,18 @@ WHERE id = #{couponId}
    - = #{currentDateTime}, true, - false) as isBetweenValidatePeriod, validate_start_date, validate_end_date ]]> from coupon where id in - ( - - #{couponIds} - - ) + ( + + #{couponIds} + + ) \ No newline at end of file From 51475f9a455d89115f8d3720f0c6d6f2e6d2e427 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 20:05:15 +0900 Subject: [PATCH 25/27] =?UTF-8?q?refactor=20:=20=EB=9E=8C=EB=8B=A4=20?= =?UTF-8?q?=ED=91=9C=ED=98=84=EC=8B=9D=EC=97=90=EC=84=9C=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=B3=80=EC=88=98=20=EC=A0=91=EA=B7=BC=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20=EA=B8=B0=EC=A1=B4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=97=90=EC=84=9C=20=EB=9E=8C=EB=8B=A4=20=ED=91=9C?= =?UTF-8?q?=ED=98=84=EC=8B=9D=EC=9D=B4=20=EC=99=B8=EB=B6=80=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20accumulatedDiscountPrice=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EC=97=AC=20"Variable=20used=20in=20lambda=20?= =?UTF-8?q?expression=20should=20be=20final=20or=20effectively=20final"=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=96=88?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.=20AtomicReference=20=EB=9E=98?= =?UTF-8?q?=ED=8D=BC=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20acc?= =?UTF-8?q?umulatedDiscountPrice=20=EB=B3=80=EC=88=98=EB=A5=BC=20=EC=95=88?= =?UTF-8?q?=EC=A0=84=ED=95=98=EA=B2=8C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/coupon_use/OrderService.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index ba56b94..1df6838 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static com.flab.offcoupon.domain.entity.OrderCoupon.createOrderCoupon; import static com.flab.offcoupon.domain.entity.OrderDetail.createOrderDetail; @@ -88,21 +89,23 @@ public ResponseDTO> getAvailableCoupons * @return */ private List filterAvailableCoupons(List availableCouponInfos) { - // 할인 가격을 누적하되 minOrderPrice를 초과하지 않는 누적 금액에 포함되는 경우만 결과 리스트에 추가 (중복 쿠폰 할인 가능용) - BigDecimal accumulatedDiscountPrice = BigDecimal.ZERO; + AtomicReference accumulatedDiscountPrice = new AtomicReference<>(BigDecimal.ZERO); List responseList = new ArrayList<>(); - for (AvailableCouponInfo info : availableCouponInfos) { + + availableCouponInfos.forEach(info -> { BigDecimal discountPrice = info.getDiscountPrice(); - accumulatedDiscountPrice = accumulatedDiscountPrice.add(discountPrice); - if (isOverThanMinOrderPrice(info.getProductPrice(), info.getMinProductPrice(), accumulatedDiscountPrice)) { + accumulatedDiscountPrice.updateAndGet(price -> price.add(discountPrice)); + if (isOverThanMinOrderPrice(info.getProductPrice(), info.getMinProductPrice(), accumulatedDiscountPrice.get())) { responseList.add(new AvailableCouponsByMemberIdResponse(info)); } else { - accumulatedDiscountPrice = accumulatedDiscountPrice.subtract(discountPrice); + accumulatedDiscountPrice.updateAndGet(price -> price.subtract(discountPrice)); } - } + }); + return responseList; } + /** * 최소 주문 금액과 비교하여 조건에 맞는 쿠폰인지 확인합니다. * From f1d69419836998bfc7207c7ef77af4b5f67bd299 Mon Sep 17 00:00:00 2001 From: sejin park Date: Mon, 15 Apr 2024 20:17:05 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refactor=20:=20=EC=86=8C=EB=82=98?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9A=B0=EB=93=9C=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offcoupon/config/security/configs/SecurityConfig.java | 1 - .../java/com/flab/offcoupon/domain/entity/Product.java | 6 +----- .../domain/entity/helper/AvailableCouponInfo.java | 4 ++-- .../dto/response/AvailableCouponsByMemberIdResponse.java | 4 ++-- .../flab/offcoupon/service/coupon_use/OrderService.java | 8 ++++---- .../com/flab/offcoupon/service/mypage/MyPageService.java | 3 +-- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java index fe380d2..b3c978e 100644 --- a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java +++ b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java @@ -90,7 +90,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(sessionManagement -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) - .maxSessionsPreventsLogin(false) .expiredUrl(LOGIN_URL) ); return http.build(); diff --git a/src/main/java/com/flab/offcoupon/domain/entity/Product.java b/src/main/java/com/flab/offcoupon/domain/entity/Product.java index 357445d..ab3dad0 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/Product.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/Product.java @@ -32,9 +32,6 @@ public final class Product { * * @return 상품의 개별 가격 */ -// public BigDecimal getPricePerUnit(Product product) { -// return product.getSalePrice().compareTo(BigDecimal.ZERO) > 0 ? product.getSalePrice() : product.getOriginalPrice(); -// } public BigDecimal getPricePerUnit() { return this.salePrice.compareTo(BigDecimal.ZERO) > 0 ? salePrice : originalPrice; } @@ -79,8 +76,7 @@ public BigDecimal calculateTotalDiscountPrice(List couponList, long quan * @return 개당 할인 가격 */ public BigDecimal calculateDiscountPricePerUnit(Coupon coupon) { - BigDecimal discountPricePerUnit = BigDecimal.ZERO; - + BigDecimal discountPricePerUnit; if (coupon.getDiscountType() == DiscountType.PERCENT) { BigDecimal discountRate = BigDecimal.valueOf(coupon.getDiscountRate()).divide(BigDecimal.valueOf(100)); discountPricePerUnit = getPricePerUnit().multiply(discountRate); diff --git a/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java index 8b61d9b..660d76d 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java @@ -26,7 +26,7 @@ public final class AvailableCouponInfo { private final BigDecimal minProductPrice; // 최소 주문 금액 private final BigDecimal productPrice; // 상품 가격 private final String discount; // 할인 쿠폰 내용 - private final BigDecimal discountPrice; // 상품 가격에 할인 쿠폰 적용한 할인가 + private final BigDecimal appliedDiscountPrice; // 상품 가격에 할인 쿠폰 적용한 할인가 private final LocalDateTime validateStartDate; private final LocalDateTime validateEndDate; private final long couponIssueId; @@ -39,7 +39,7 @@ public AvailableCouponInfo(AvailableCouponsByMemberIdVo vo) { this.minProductPrice = vo.minOrderPrice(); this.productPrice = productPrice(vo); this.discount = DiscountUtils.getDiscountInfo(vo.discountType(), vo.discountRate(), vo.discountPrice()); - this.discountPrice = calculateDiscountPrice(vo); + this.appliedDiscountPrice = calculateDiscountPrice(vo); this.validateStartDate = vo.validateStartDate(); this.validateEndDate = vo.validateEndDate(); this.couponIssueId = vo.couponIssueId(); diff --git a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java index d2b0483..bbb0a08 100644 --- a/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java +++ b/src/main/java/com/flab/offcoupon/dto/response/AvailableCouponsByMemberIdResponse.java @@ -15,7 +15,7 @@ public final class AvailableCouponsByMemberIdResponse { private final String category; private final String description; private final String discount; // 할인 쿠폰 내용 - private final BigDecimal discountPrice; // 상품 가격에 할인 쿠폰 적용한 할인가 + private final BigDecimal appliedDiscountPrice; // 상품 가격에 할인 쿠폰 적용한 할인가 private final LocalDateTime validateStartDate; private final LocalDateTime validateEndDate; private final long couponIssueId; @@ -27,7 +27,7 @@ public AvailableCouponsByMemberIdResponse(AvailableCouponInfo info) { this.category = info.getCategory(); this.description = info.getDescription(); this.discount = info.getDiscount(); - this.discountPrice = info.getDiscountPrice(); + this.appliedDiscountPrice = info.getAppliedDiscountPrice(); this.validateStartDate = info.getValidateStartDate(); this.validateEndDate = info.getValidateEndDate(); this.couponIssueId = info.getCouponIssueId(); diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 1df6838..3273331 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -71,7 +71,7 @@ public ResponseDTO> getAvailableCoupons // 할인가격 기준으로 내림차순 List availableCouponInfos = availableCouponData.stream() .map(AvailableCouponInfo::new) - .sorted(Comparator.comparing(AvailableCouponInfo::getDiscountPrice).reversed()) + .sorted(Comparator.comparing(AvailableCouponInfo::getAppliedDiscountPrice).reversed()) .toList(); return ResponseDTO.getSuccessResult(filterAvailableCoupons(availableCouponInfos)); @@ -93,7 +93,7 @@ private List filterAvailableCoupons(List responseList = new ArrayList<>(); availableCouponInfos.forEach(info -> { - BigDecimal discountPrice = info.getDiscountPrice(); + BigDecimal discountPrice = info.getAppliedDiscountPrice(); accumulatedDiscountPrice.updateAndGet(price -> price.add(discountPrice)); if (isOverThanMinOrderPrice(info.getProductPrice(), info.getMinProductPrice(), accumulatedDiscountPrice.get())) { responseList.add(new AvailableCouponsByMemberIdResponse(info)); @@ -116,8 +116,8 @@ private List filterAvailableCoupons(List= 0; + BigDecimal appliedDiscountPriceToProduct = price.subtract(accumulatedDiscountPrice); + return appliedDiscountPriceToProduct.compareTo(minOrderPrice) >= 0; } /** diff --git a/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java b/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java index 87a22c6..59dd5e2 100644 --- a/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java +++ b/src/main/java/com/flab/offcoupon/service/mypage/MyPageService.java @@ -7,7 +7,6 @@ import org.springframework.stereotype.Service; import java.util.List; -import java.util.stream.Collectors; @RequiredArgsConstructor @Service @@ -19,6 +18,6 @@ public ResponseDTO> getAllCoupons(long member return ResponseDTO.getSuccessResult(couponIssueRepository.getAllCoupons(memberId) .stream() .map(AllCouponsByMemberIdResponse::new) - .collect(Collectors.toList())); + .toList()); } } From c0a419d93c6bbbf1d182c3f4eaed7d58dd76842f Mon Sep 17 00:00:00 2001 From: sejin park Date: Thu, 18 Apr 2024 07:58:48 +0900 Subject: [PATCH 27/27] =?UTF-8?q?refactor=20:=20early=20return=20&=20strea?= =?UTF-8?q?m=20API=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flab/offcoupon/config/redis/SessionConfig.java | 14 +++++++++----- .../config/security/configs/SecurityConfig.java | 8 +------- .../controller/CouponIssueController.java | 2 +- .../offcoupon/controller/MyPageController.java | 1 - .../flab/offcoupon/controller/OrderController.java | 7 +++---- .../domain/entity/helper/AvailableCouponInfo.java | 3 +-- .../offcoupon/service/coupon_use/OrderService.java | 8 +++----- 7 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/flab/offcoupon/config/redis/SessionConfig.java b/src/main/java/com/flab/offcoupon/config/redis/SessionConfig.java index 9ae86c7..17be426 100644 --- a/src/main/java/com/flab/offcoupon/config/redis/SessionConfig.java +++ b/src/main/java/com/flab/offcoupon/config/redis/SessionConfig.java @@ -1,5 +1,7 @@ package com.flab.offcoupon.config.redis; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; @@ -11,12 +13,13 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; @Configuration +@RequiredArgsConstructor @EnableRedisHttpSession public class SessionConfig { @@ -30,6 +33,7 @@ public class SessionConfig { private String redisPassword; private static final String REDISSON_HOST_PREFIX = "redis://"; + private final ObjectMapper objectMapper; /** *

    Lettuce: Lettuce는 Redis와의 비동기 및 논 블로킹 I/O를 지원하는 자바 라이브러리입니다.

    * 해당 빈은 세션 저장소로 사용하기 위해 LettuceConnectionFactory를 생성합니다.
    @@ -75,16 +79,16 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashValueSerializer(springSessionDefaultRedisSerializer()); + redisTemplate.setHashValueSerializer(springSessionDefaultRedisSerializer(objectMapper)); /* Redis Pub/Sub기능에서 Message 직렬화를 위해 추가 */ redisTemplate.setKeySerializer(RedisSerializer.string()); - redisTemplate.setValueSerializer(springSessionDefaultRedisSerializer()); + redisTemplate.setValueSerializer(springSessionDefaultRedisSerializer(objectMapper)); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean - public RedisSerializer springSessionDefaultRedisSerializer(){ - return new Jackson2JsonRedisSerializer<>(Object.class); + public RedisSerializer springSessionDefaultRedisSerializer(ObjectMapper objectMapper) { + return new GenericJackson2JsonRedisSerializer(objectMapper); } /** * RedisMessageListenerContainer는 Spring Data Redis에서 제공하는 클래스로 Redis Pub/Sub 메시지를 처리하는 컨테이너입니다. diff --git a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java index b3c978e..c0d6575 100644 --- a/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java +++ b/src/main/java/com/flab/offcoupon/config/security/configs/SecurityConfig.java @@ -29,7 +29,6 @@ public class SecurityConfig { private final AuthenticationFailureHandler customAuthenticationFailureHandler; private final AccessDeniedHandler customAccessDeniedHandler; private static final String LOGIN_URL = "/api/v1/members/login"; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http @@ -80,18 +79,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .exceptionHandling(exceptionHandling -> exceptionHandling .accessDeniedHandler(customAccessDeniedHandler) ); - http. - rememberMe(rememberMe -> rememberMe - .rememberMeCookieName("remember") - .tokenValiditySeconds(3600) - .userDetailsService(userDetailsService) - ); http .sessionManagement(sessionManagement -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) .expiredUrl(LOGIN_URL) ); + return http.build(); } @Bean diff --git a/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java b/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java index 42a3de2..2f3c4f9 100644 --- a/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java +++ b/src/main/java/com/flab/offcoupon/controller/CouponIssueController.java @@ -21,7 +21,7 @@ *

    c. 검증이 완료된 요청은 RabbitMQ를 활용해 대기큐에 적재한다

    *

    d. 스케줄링을 통해 Queue에 저장된 메세지를 하나씩 꺼내어 쿠폰 발급 히스토리 INSERT, 총 쿠폰 발급 수량 UPDATE

    *

    e. 메세지를 하나씩 꺼내고 이력을 저장할때마다 해당 유저에게 SSE 알람 전송

    - *

    정리 : 유저 트래픽과 쿠폰 발급 트랜잭션 분리 -> 목표 : Redis를 통한 트래픽 대응 및 MySQL 트래픽 제어

    + *

    정리 : 유저 트래픽과 쿠폰 발급 트랜잭션 분리 -> 목표 : Redis를 통한 트래픽 대응 및 MySQL 부하 제어

    * */ @RequiredArgsConstructor diff --git a/src/main/java/com/flab/offcoupon/controller/MyPageController.java b/src/main/java/com/flab/offcoupon/controller/MyPageController.java index d9f4d39..334f279 100644 --- a/src/main/java/com/flab/offcoupon/controller/MyPageController.java +++ b/src/main/java/com/flab/offcoupon/controller/MyPageController.java @@ -16,7 +16,6 @@ public class MyPageController { private final MyPageService myPageService; - @GetMapping("/coupons") public ResponseEntity>> getAllCoupons(@RequestParam final long memberId) { return ResponseEntity.status(HttpStatus.OK).body(myPageService.getAllCoupons(memberId)); } diff --git a/src/main/java/com/flab/offcoupon/controller/OrderController.java b/src/main/java/com/flab/offcoupon/controller/OrderController.java index 7cdbbd2..5cbbaa5 100644 --- a/src/main/java/com/flab/offcoupon/controller/OrderController.java +++ b/src/main/java/com/flab/offcoupon/controller/OrderController.java @@ -26,7 +26,6 @@ public class OrderController { * @param productId 상품 ID * @return 사용 가능한 쿠폰 목록 */ - @GetMapping("/available-coupons") public ResponseEntity>> getAvailableCoupons(@RequestParam final long memberId, @RequestParam final long productId) { LocalDateTime now = LocalDateTime.now(); @@ -37,14 +36,14 @@ public ResponseEntity>> get * 상품 주문
    * 결제 SDK 연동은 현재 진행 중인 프로젝트에서 메인으로 다루지 않기 때문에 제외했습니다. * - * @param productId 상품 ID + * @param productId 상품 ID * @param orderProductRequest 주문 요청 정보 * @return 주문 성공 여부 */ @PostMapping("/products/{productId}") public ResponseEntity> orderProduct(@PathVariable final long productId, - @RequestBody final OrderProductRequest orderProductRequest) { + @RequestBody final OrderProductRequest orderProductRequest) { LocalDateTime now = LocalDateTime.now(); - return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.getSuccessResult(orderService.orderProduct(productId,orderProductRequest, now))); + return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.getSuccessResult(orderService.orderProduct(productId, orderProductRequest, now))); } } diff --git a/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java b/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java index 660d76d..784a931 100644 --- a/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java +++ b/src/main/java/com/flab/offcoupon/domain/entity/helper/AvailableCouponInfo.java @@ -78,9 +78,8 @@ public BigDecimal calculateDiscountPrice(AvailableCouponsByMemberIdVo vo) { long discountPrice = vo.discountPrice() == null ? 0 : vo.discountPrice(); if (isSalePriceOverThanZero(vo.salePrice())) { return calculateDiscountPrice(vo.discountType(), vo.salePrice(), discountRate, discountPrice); - } else { - return calculateDiscountPrice(vo.discountType(), vo.originalPrice(), discountRate, discountPrice); } + return calculateDiscountPrice(vo.discountType(), vo.originalPrice(), discountRate, discountPrice); } /** diff --git a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java index 3273331..2ba9546 100644 --- a/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java +++ b/src/main/java/com/flab/offcoupon/service/coupon_use/OrderService.java @@ -165,11 +165,9 @@ private void validateCouponIsAvailable(OrderProductRequest request, LocalDateTim // 2. 현재 시간이 쿠폰의 유효기간 범위내에 있는지 확인 List isBetweenValidatePeriodVo = couponRepository.getCouponValidationPeriod(request.getCouponId()); - for (CouponValidationPeriodVo validationPeriod : isBetweenValidatePeriodVo) { - if (!isNowBetweenValidatePeriod(now, validationPeriod)) { - throw new CouponUsageInvalidPeriodException(COUPON_USAGE_INVALID_PERIOD - .formatted(validationPeriod.couponId(), validationPeriod.validateStartDate(), validationPeriod.validateEndDate())); - } + if (isBetweenValidatePeriodVo.stream().noneMatch(validationPeriod -> isNowBetweenValidatePeriod(now, validationPeriod))) { + throw new CouponUsageInvalidPeriodException(COUPON_USAGE_INVALID_PERIOD + .formatted(isBetweenValidatePeriodVo.get(0).couponId(), isBetweenValidatePeriodVo.get(0).validateStartDate(), isBetweenValidatePeriodVo.get(0).validateEndDate())); } } private boolean isNowBetweenValidatePeriod(LocalDateTime now, CouponValidationPeriodVo validationPeriod) {