From 072f8812406d42ad1167bb0baa6594f76677af47 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 10:44:38 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20Coupon=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/coupon/domain/Category.java | 6 +++ .../java/coupon/domain/coupon/Coupon.java | 23 ++++++++++ .../java/coupon/domain/coupon/CouponName.java | 25 ++++++++++ .../coupon/domain/coupon/DiscountMount.java | 30 ++++++++++++ .../coupon/domain/coupon/MinimumMount.java | 26 +++++++++++ .../java/coupon/domain/coupon/Period.java | 23 ++++++++++ .../coupon/domain/coupon/CouponNameTest.java | 36 +++++++++++++++ .../domain/coupon/DiscountMountTest.java | 46 +++++++++++++++++++ .../domain/coupon/MinimumMountTest.java | 37 +++++++++++++++ .../java/coupon/domain/coupon/PeriodTest.java | 21 +++++++++ 10 files changed, 273 insertions(+) create mode 100644 src/main/java/coupon/domain/Category.java create mode 100644 src/main/java/coupon/domain/coupon/Coupon.java create mode 100644 src/main/java/coupon/domain/coupon/CouponName.java create mode 100644 src/main/java/coupon/domain/coupon/DiscountMount.java create mode 100644 src/main/java/coupon/domain/coupon/MinimumMount.java create mode 100644 src/main/java/coupon/domain/coupon/Period.java create mode 100644 src/test/java/coupon/domain/coupon/CouponNameTest.java create mode 100644 src/test/java/coupon/domain/coupon/DiscountMountTest.java create mode 100644 src/test/java/coupon/domain/coupon/MinimumMountTest.java create mode 100644 src/test/java/coupon/domain/coupon/PeriodTest.java diff --git a/src/main/java/coupon/domain/Category.java b/src/main/java/coupon/domain/Category.java new file mode 100644 index 00000000..12a52a10 --- /dev/null +++ b/src/main/java/coupon/domain/Category.java @@ -0,0 +1,6 @@ +package coupon.domain; + +public enum Category { + + FASHION, HOME_APPLIANCE, FURNITURE, FOOD +} diff --git a/src/main/java/coupon/domain/coupon/Coupon.java b/src/main/java/coupon/domain/coupon/Coupon.java new file mode 100644 index 00000000..0fcf6126 --- /dev/null +++ b/src/main/java/coupon/domain/coupon/Coupon.java @@ -0,0 +1,23 @@ +package coupon.domain.coupon; + +import coupon.domain.Category; +import lombok.Getter; + +@Getter +public class Coupon { + + private final CouponName couponName; + private final DiscountMount discountMount; + private final MinimumMount minimumMount; + private final Category category; + private final Period period; + + public Coupon(CouponName couponName, DiscountMount discountMount, MinimumMount minimumMount, Category category, + Period period) { + this.couponName = couponName; + this.discountMount = discountMount; + this.minimumMount = minimumMount; + this.category = category; + this.period = period; + } +} diff --git a/src/main/java/coupon/domain/coupon/CouponName.java b/src/main/java/coupon/domain/coupon/CouponName.java new file mode 100644 index 00000000..91aaa622 --- /dev/null +++ b/src/main/java/coupon/domain/coupon/CouponName.java @@ -0,0 +1,25 @@ +package coupon.domain.coupon; + +import lombok.Getter; + +@Getter +public class CouponName { + + private static final int MAX_LENGTH = 30; + + private final String name; + + public CouponName(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("이름은 비어있을 수 없습니다."); + } + if (name.length() > MAX_LENGTH) { + throw new IllegalArgumentException("이름은 최대 " + MAX_LENGTH + "자 입니다."); + } + } +} diff --git a/src/main/java/coupon/domain/coupon/DiscountMount.java b/src/main/java/coupon/domain/coupon/DiscountMount.java new file mode 100644 index 00000000..b4b68f19 --- /dev/null +++ b/src/main/java/coupon/domain/coupon/DiscountMount.java @@ -0,0 +1,30 @@ +package coupon.domain.coupon; + +import lombok.Getter; + +@Getter +public class DiscountMount { + + private static final int MINIMUM_MOUNT = 1_000; + private static final int MAXIMUM_MOUNT = 10_000; + private static final int UNIT = 500; + + private final int mount; + + public DiscountMount(int mount) { + validate(mount); + this.mount = mount; + } + + private void validate(int mount) { + if (mount < MINIMUM_MOUNT) { + throw new IllegalArgumentException("할인 금액은 " + MINIMUM_MOUNT + "원 이상이어야 합니다."); + } + if (mount > MAXIMUM_MOUNT) { + throw new IllegalArgumentException("할인 금액은 " + MAXIMUM_MOUNT + "원 이하여야 합니다."); + } + if (mount % UNIT != 0) { + throw new IllegalArgumentException("할인 금액은 " + UNIT + "원 단위여야 합니다."); + } + } +} diff --git a/src/main/java/coupon/domain/coupon/MinimumMount.java b/src/main/java/coupon/domain/coupon/MinimumMount.java new file mode 100644 index 00000000..86b7f5b7 --- /dev/null +++ b/src/main/java/coupon/domain/coupon/MinimumMount.java @@ -0,0 +1,26 @@ +package coupon.domain.coupon; + +import lombok.Getter; + +@Getter +public class MinimumMount { + + private static final int MINIMUM_MOUNT = 5_000; + private static final int MAXIMUM_MOUNT = 100_000; + + private final int mount; + + public MinimumMount(int mount) { + validate(mount); + this.mount = mount; + } + + private void validate(int mount) { + if (mount < MINIMUM_MOUNT) { + throw new IllegalArgumentException("최소 주문 금액은 " + MINIMUM_MOUNT + "원 이상이어야 합니다."); + } + if (mount > MAXIMUM_MOUNT) { + throw new IllegalArgumentException("최소 주문 금액은 " + MAXIMUM_MOUNT + "원 이하여야 합니다."); + } + } +} diff --git a/src/main/java/coupon/domain/coupon/Period.java b/src/main/java/coupon/domain/coupon/Period.java new file mode 100644 index 00000000..2b3e7fca --- /dev/null +++ b/src/main/java/coupon/domain/coupon/Period.java @@ -0,0 +1,23 @@ +package coupon.domain.coupon; + +import java.time.LocalDate; +import lombok.Getter; + +@Getter +public class Period { + + private final LocalDate startDate; + private final LocalDate endDate; + + public Period(LocalDate startDate, LocalDate endDate) { + validate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validate(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException("시작일은 종료일보다 이전이어야 합니다."); + } + } +} diff --git a/src/test/java/coupon/domain/coupon/CouponNameTest.java b/src/test/java/coupon/domain/coupon/CouponNameTest.java new file mode 100644 index 00000000..a15e2811 --- /dev/null +++ b/src/test/java/coupon/domain/coupon/CouponNameTest.java @@ -0,0 +1,36 @@ +package coupon.domain.coupon; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CouponNameTest { + + @Test + @DisplayName("null 값으로 이름을 생성할 경우 예외로 처리한다.") + void nullName() { + String nullSource = null; + assertThatThrownBy(() -> new CouponName(nullSource)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("이름이 0자인 경우 예외로 처리한다.") + void zeroLength() { + String emptyName = ""; + assertThatThrownBy(() -> new CouponName(emptyName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("이름이 30자를 초과하면 예외로 처리한다.") + void mountAboveMaximum() { + String tooLongName = "0123456789012345678901234567890"; + assertThatThrownBy(() -> new CouponName(tooLongName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 최대 30자 입니다."); + } +} diff --git a/src/test/java/coupon/domain/coupon/DiscountMountTest.java b/src/test/java/coupon/domain/coupon/DiscountMountTest.java new file mode 100644 index 00000000..ecced07d --- /dev/null +++ b/src/test/java/coupon/domain/coupon/DiscountMountTest.java @@ -0,0 +1,46 @@ +package coupon.domain.coupon; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DiscountMountTest { + + @ParameterizedTest + @ValueSource(ints = {1000, 1500, 2000, 5000, 10000}) + @DisplayName("유효한 할인 금액을 통해 생성할 수 있다.") + void validMountCreation(int mount) { + assertDoesNotThrow(() -> new DiscountMount(mount)); + } + + @Test + @DisplayName("최소 할인 금액 미만이면 예외로 처리한다.") + void mountBelowMinimum() { + int invalidMount = 500; + assertThatThrownBy(() -> new DiscountMount(invalidMount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("할인 금액은 1000원 이상이어야 합니다."); + } + + @Test + @DisplayName("최대 할인 금액을 초과하면 예외로 처리한다") + void mountAboveMaximum() { + int invalidMount = 15000; + assertThatThrownBy(() -> new DiscountMount(invalidMount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("할인 금액은 10000원 이하여야 합니다."); + } + + @Test + @DisplayName("할인 금액이 500원 단위가 아닐 경우 예외로 처리한다") + void mountNotMultipleOfUnit() { + int invalidMount = 1250; + assertThatThrownBy(() -> new DiscountMount(invalidMount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("할인 금액은 500원 단위여야 합니다."); + } +} diff --git a/src/test/java/coupon/domain/coupon/MinimumMountTest.java b/src/test/java/coupon/domain/coupon/MinimumMountTest.java new file mode 100644 index 00000000..e947e6a3 --- /dev/null +++ b/src/test/java/coupon/domain/coupon/MinimumMountTest.java @@ -0,0 +1,37 @@ +package coupon.domain.coupon; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MinimumMountTest { + + @ParameterizedTest + @ValueSource(ints = {5000, 5555, 6000, 50000, 100000}) + @DisplayName("유효한 할인 금액을 통해 생성할 수 있다.") + void validMountCreation(int mount) { + assertDoesNotThrow(() -> new MinimumMount(mount)); + } + + @Test + @DisplayName("최소 할인 금액 미만이면 예외로 처리한다.") + void mountBelowMinimum() { + int invalidMount = 4000; + assertThatThrownBy(() -> new MinimumMount(invalidMount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("최소 주문 금액은 5000원 이상이어야 합니다."); + } + + @Test + @DisplayName("최대 할인 금액을 초과하면 예외로 처리한다") + void mountAboveMaximum() { + int invalidMount = 110000; + assertThatThrownBy(() -> new MinimumMount(invalidMount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("최소 주문 금액은 100000원 이하여야 합니다."); + } +} diff --git a/src/test/java/coupon/domain/coupon/PeriodTest.java b/src/test/java/coupon/domain/coupon/PeriodTest.java new file mode 100644 index 00000000..85d6be7b --- /dev/null +++ b/src/test/java/coupon/domain/coupon/PeriodTest.java @@ -0,0 +1,21 @@ +package coupon.domain.coupon; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PeriodTest { + + @Test + @DisplayName("시작일이 종료일보다 이후일 때 예외 발생") + void invalidPeriodCreation() { + LocalDate startDate = LocalDate.now(); + LocalDate endDate = startDate.minusDays(1); + + assertThatThrownBy(() -> new Period(startDate, endDate)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시작일은 종료일보다 이전이어야 합니다."); + } +} From 9c273958911272716f7f270c8274cdaf7de5f2a7 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 10:46:37 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20Member=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/coupon/domain/member/Member.java | 15 ++++++++ .../java/coupon/domain/member/MemberName.java | 25 +++++++++++++ .../coupon/domain/member/MemberNameTest.java | 37 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/main/java/coupon/domain/member/Member.java create mode 100644 src/main/java/coupon/domain/member/MemberName.java create mode 100644 src/test/java/coupon/domain/member/MemberNameTest.java diff --git a/src/main/java/coupon/domain/member/Member.java b/src/main/java/coupon/domain/member/Member.java new file mode 100644 index 00000000..bcd2d2c5 --- /dev/null +++ b/src/main/java/coupon/domain/member/Member.java @@ -0,0 +1,15 @@ +package coupon.domain.member; + +import lombok.Getter; + +@Getter +public class Member { + + private final Long id; + private final MemberName memberName; + + public Member(Long id, MemberName memberName) { + this.id = id; + this.memberName = memberName; + } +} diff --git a/src/main/java/coupon/domain/member/MemberName.java b/src/main/java/coupon/domain/member/MemberName.java new file mode 100644 index 00000000..1eb2166e --- /dev/null +++ b/src/main/java/coupon/domain/member/MemberName.java @@ -0,0 +1,25 @@ +package coupon.domain.member; + +import lombok.Getter; + +@Getter +public class MemberName { + + private static final int MAX_LENGTH = 30; + + private final String name; + + public MemberName(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("이름은 비어있을 수 없습니다."); + } + if (name.length() > MAX_LENGTH) { + throw new IllegalArgumentException("이름은 최대 " + MAX_LENGTH + "자 입니다."); + } + } +} diff --git a/src/test/java/coupon/domain/member/MemberNameTest.java b/src/test/java/coupon/domain/member/MemberNameTest.java new file mode 100644 index 00000000..98b66706 --- /dev/null +++ b/src/test/java/coupon/domain/member/MemberNameTest.java @@ -0,0 +1,37 @@ +package coupon.domain.member; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MemberNameTest { + + + @Test + @DisplayName("null 값으로 이름을 생성할 경우 예외로 처리한다.") + void nullName() { + String nullSource = null; + assertThatThrownBy(() -> new MemberName(nullSource)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("이름이 0자인 경우 예외로 처리한다.") + void zeroLength() { + String emptyName = ""; + assertThatThrownBy(() -> new MemberName(emptyName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("이름이 30자를 초과하면 예외로 처리한다.") + void mountAboveMaximum() { + String tooLongName = "이무송은포케이무송은포케이무송은포케이무송은포케이무송은포케이무송은포케이무송은포케이무송은포케이무송은포케"; + assertThatThrownBy(() -> new MemberName(tooLongName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 최대 30자 입니다."); + } +} From 4f0e6305bfda9fa48d4b067690029337a1f4a8e9 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 10:46:47 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20MemberCoupon=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/coupon/domain/MemberCoupon.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/coupon/domain/MemberCoupon.java diff --git a/src/main/java/coupon/domain/MemberCoupon.java b/src/main/java/coupon/domain/MemberCoupon.java new file mode 100644 index 00000000..fcdbdb71 --- /dev/null +++ b/src/main/java/coupon/domain/MemberCoupon.java @@ -0,0 +1,24 @@ +package coupon.domain; + +import java.time.LocalDate; +import lombok.Getter; + +@Getter +public class MemberCoupon { + + private final Long id; + private final Long couponId; + private final Long memberId; + private final boolean used; + private final LocalDate issueDate; + private final LocalDate expirationDate; + + public MemberCoupon(Long id, Long couponId, Long memberId, boolean used, LocalDate issueDate) { + this.id = id; + this.couponId = couponId; + this.memberId = memberId; + this.used = used; + this.issueDate = issueDate; + this.expirationDate = issueDate.plusDays(6); + } +} From 6bb38a7001f6700448904cd52b4d1976eadaf317 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 12:37:29 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=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 --- src/main/java/coupon/domain/MemberCoupon.java | 37 ++++++++++++++----- .../java/coupon/domain/coupon/Coupon.java | 26 ++++++++++--- .../java/coupon/domain/member/Member.java | 16 +++++++- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/main/java/coupon/domain/MemberCoupon.java b/src/main/java/coupon/domain/MemberCoupon.java index fcdbdb71..ddf4d5b2 100644 --- a/src/main/java/coupon/domain/MemberCoupon.java +++ b/src/main/java/coupon/domain/MemberCoupon.java @@ -1,22 +1,41 @@ package coupon.domain; +import coupon.domain.coupon.Coupon; +import coupon.domain.member.Member; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import java.time.LocalDate; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class MemberCoupon { - private final Long id; - private final Long couponId; - private final Long memberId; - private final boolean used; - private final LocalDate issueDate; - private final LocalDate expirationDate; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "coupon_id") + private Coupon coupon; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + private boolean used; + private LocalDate issueDate; + private LocalDate expirationDate; - public MemberCoupon(Long id, Long couponId, Long memberId, boolean used, LocalDate issueDate) { + public MemberCoupon(Long id, Coupon coupon, Member member, boolean used, LocalDate issueDate) { this.id = id; - this.couponId = couponId; - this.memberId = memberId; + this.coupon = coupon; + this.member = member; this.used = used; this.issueDate = issueDate; this.expirationDate = issueDate.plusDays(6); diff --git a/src/main/java/coupon/domain/coupon/Coupon.java b/src/main/java/coupon/domain/coupon/Coupon.java index 0fcf6126..d33d71f4 100644 --- a/src/main/java/coupon/domain/coupon/Coupon.java +++ b/src/main/java/coupon/domain/coupon/Coupon.java @@ -1,16 +1,32 @@ package coupon.domain.coupon; import coupon.domain.Category; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Coupon { - private final CouponName couponName; - private final DiscountMount discountMount; - private final MinimumMount minimumMount; - private final Category category; - private final Period period; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Embedded + private CouponName couponName; + @Embedded + private DiscountMount discountMount; + @Embedded + private MinimumMount minimumMount; + private Category category; + @Embedded + private Period period; public Coupon(CouponName couponName, DiscountMount discountMount, MinimumMount minimumMount, Category category, Period period) { diff --git a/src/main/java/coupon/domain/member/Member.java b/src/main/java/coupon/domain/member/Member.java index bcd2d2c5..3c15601d 100644 --- a/src/main/java/coupon/domain/member/Member.java +++ b/src/main/java/coupon/domain/member/Member.java @@ -1,12 +1,24 @@ package coupon.domain.member; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { - private final Long id; - private final MemberName memberName; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Embedded + private MemberName memberName; public Member(Long id, MemberName memberName) { this.id = id; From 1479fb5d585906ed5d0bc4c1c87b06f5bb4f1f00 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 14:31:58 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/coupon/DataSourceConfig.java | 68 +++++++++++++++++++ .../java/coupon/ReadOnlyDataSourceRouter.java | 15 ++++ .../java/coupon/domain/coupon/CouponName.java | 10 +-- .../coupon/domain/coupon/DiscountMount.java | 10 +-- .../coupon/domain/coupon/MinimumMount.java | 10 +-- .../java/coupon/domain/coupon/Period.java | 6 +- .../coupon/repository/CouponRepository.java | 9 +++ .../java/coupon/service/CouponService.java | 24 +++++++ src/main/resources/application.yml | 2 +- .../coupon/service/CouponServiceTest.java | 31 +++++++++ 10 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 src/main/java/coupon/DataSourceConfig.java create mode 100644 src/main/java/coupon/ReadOnlyDataSourceRouter.java create mode 100644 src/main/java/coupon/repository/CouponRepository.java create mode 100644 src/main/java/coupon/service/CouponService.java create mode 100644 src/test/java/coupon/service/CouponServiceTest.java diff --git a/src/main/java/coupon/DataSourceConfig.java b/src/main/java/coupon/DataSourceConfig.java new file mode 100644 index 00000000..b64a303a --- /dev/null +++ b/src/main/java/coupon/DataSourceConfig.java @@ -0,0 +1,68 @@ +package coupon; + +import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.EntityManagerFactory; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class DataSourceConfig { + + public static final String READER = "reader"; + public static final String WRITER = "writer"; + + @ConfigurationProperties(prefix = "coupon.datasource.writer") + @Bean + public DataSource writerDataSource() { + return DataSourceBuilder.create().type(HikariDataSource.class).build(); + } + + @ConfigurationProperties(prefix = "coupon.datasource.reader") + @Bean + public DataSource readerDataSource() { + return DataSourceBuilder.create().type(HikariDataSource.class).build(); + } + + @DependsOn({"writerDataSource", "readerDataSource"}) + @Bean + public DataSource routingDataSource( + @Qualifier("writerDataSource") DataSource writer, + @Qualifier("readerDataSource") DataSource reader) { + ReadOnlyDataSourceRouter routingDataSource = new ReadOnlyDataSourceRouter(); + + Map dataSourceMap = new HashMap<>(); + + dataSourceMap.put(WRITER, writer); + dataSourceMap.put(READER, reader); + + routingDataSource.setTargetDataSources(dataSourceMap); + routingDataSource.setDefaultTargetDataSource(writer); + + return routingDataSource; + } + + @DependsOn({"routingDataSource"}) + @Primary + @Bean + public DataSource dataSource(DataSource routingDataSource) { + return new LazyConnectionDataSourceProxy(routingDataSource); + } + + @Bean + public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); + jpaTransactionManager.setEntityManagerFactory(entityManagerFactory); + return jpaTransactionManager; + } +} diff --git a/src/main/java/coupon/ReadOnlyDataSourceRouter.java b/src/main/java/coupon/ReadOnlyDataSourceRouter.java new file mode 100644 index 00000000..bf11c3e3 --- /dev/null +++ b/src/main/java/coupon/ReadOnlyDataSourceRouter.java @@ -0,0 +1,15 @@ +package coupon; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class ReadOnlyDataSourceRouter extends AbstractRoutingDataSource { + + @Override + protected Object determineCurrentLookupKey() { + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + return DataSourceConfig.READER; + } + return DataSourceConfig.WRITER; + } +} diff --git a/src/main/java/coupon/domain/coupon/CouponName.java b/src/main/java/coupon/domain/coupon/CouponName.java index 91aaa622..7982009b 100644 --- a/src/main/java/coupon/domain/coupon/CouponName.java +++ b/src/main/java/coupon/domain/coupon/CouponName.java @@ -1,17 +1,19 @@ package coupon.domain.coupon; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class CouponName { private static final int MAX_LENGTH = 30; - private final String name; + private String couponName; - public CouponName(String name) { - validate(name); - this.name = name; + public CouponName(String couponName) { + validate(couponName); + this.couponName = couponName; } private void validate(String name) { diff --git a/src/main/java/coupon/domain/coupon/DiscountMount.java b/src/main/java/coupon/domain/coupon/DiscountMount.java index b4b68f19..6b847407 100644 --- a/src/main/java/coupon/domain/coupon/DiscountMount.java +++ b/src/main/java/coupon/domain/coupon/DiscountMount.java @@ -1,19 +1,21 @@ package coupon.domain.coupon; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class DiscountMount { private static final int MINIMUM_MOUNT = 1_000; private static final int MAXIMUM_MOUNT = 10_000; private static final int UNIT = 500; - private final int mount; + private int discountMount; - public DiscountMount(int mount) { - validate(mount); - this.mount = mount; + public DiscountMount(int discountMount) { + validate(discountMount); + this.discountMount = discountMount; } private void validate(int mount) { diff --git a/src/main/java/coupon/domain/coupon/MinimumMount.java b/src/main/java/coupon/domain/coupon/MinimumMount.java index 86b7f5b7..2782aee7 100644 --- a/src/main/java/coupon/domain/coupon/MinimumMount.java +++ b/src/main/java/coupon/domain/coupon/MinimumMount.java @@ -1,18 +1,20 @@ package coupon.domain.coupon; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class MinimumMount { private static final int MINIMUM_MOUNT = 5_000; private static final int MAXIMUM_MOUNT = 100_000; - private final int mount; + private int minimumMount; - public MinimumMount(int mount) { - validate(mount); - this.mount = mount; + public MinimumMount(int minimumMount) { + validate(minimumMount); + this.minimumMount = minimumMount; } private void validate(int mount) { diff --git a/src/main/java/coupon/domain/coupon/Period.java b/src/main/java/coupon/domain/coupon/Period.java index 2b3e7fca..16323b86 100644 --- a/src/main/java/coupon/domain/coupon/Period.java +++ b/src/main/java/coupon/domain/coupon/Period.java @@ -2,12 +2,14 @@ import java.time.LocalDate; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class Period { - private final LocalDate startDate; - private final LocalDate endDate; + private LocalDate startDate; + private LocalDate endDate; public Period(LocalDate startDate, LocalDate endDate) { validate(startDate, endDate); diff --git a/src/main/java/coupon/repository/CouponRepository.java b/src/main/java/coupon/repository/CouponRepository.java new file mode 100644 index 00000000..cdd0bbaa --- /dev/null +++ b/src/main/java/coupon/repository/CouponRepository.java @@ -0,0 +1,9 @@ +package coupon.repository; + +import coupon.domain.coupon.Coupon; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CouponRepository extends JpaRepository { +} diff --git a/src/main/java/coupon/service/CouponService.java b/src/main/java/coupon/service/CouponService.java new file mode 100644 index 00000000..aef506b1 --- /dev/null +++ b/src/main/java/coupon/service/CouponService.java @@ -0,0 +1,24 @@ +package coupon.service; + +import coupon.domain.coupon.Coupon; +import coupon.repository.CouponRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CouponService { + + private final CouponRepository couponRepository; + + public void create(Coupon coupon) { + couponRepository.save(coupon); + } + + @Transactional(readOnly = true) + public Coupon getCoupon(Long couponId) { + return couponRepository.findById(couponId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 쿠폰 id입니다.")); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 54f7c419..82708c37 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: format_sql: true show_sql: false use_sql_comments: true - hbm2ddl.auto: validate + hbm2ddl.auto: update check_nullability: true query.in_clause_parameter_padding: true open-in-view: false diff --git a/src/test/java/coupon/service/CouponServiceTest.java b/src/test/java/coupon/service/CouponServiceTest.java new file mode 100644 index 00000000..f9775a23 --- /dev/null +++ b/src/test/java/coupon/service/CouponServiceTest.java @@ -0,0 +1,31 @@ +package coupon.service; + +import coupon.domain.Category; +import coupon.domain.coupon.Coupon; +import coupon.domain.coupon.CouponName; +import coupon.domain.coupon.DiscountMount; +import coupon.domain.coupon.MinimumMount; +import coupon.domain.coupon.Period; +import java.time.LocalDate; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CouponServiceTest { + + @Autowired + private CouponService couponService; + + @Test + void create() { + Coupon coupon = new Coupon( + new CouponName("쿠폰"), + new DiscountMount(1000), + new MinimumMount(5000), + Category.FASHION, + new Period(LocalDate.now().minusDays(1), LocalDate.now().plusDays(1)) + ); + couponService.create(coupon); + } +} From 0b9689806d2691bab3c7b794c573a60e4599e62d Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 14:47:10 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EC=BF=A0=ED=8F=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=EA=B0=80=20=EC=BF=A0?= =?UTF-8?q?=ED=8F=B0=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/coupon/service/CouponService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/coupon/service/CouponService.java b/src/main/java/coupon/service/CouponService.java index aef506b1..e2d497de 100644 --- a/src/main/java/coupon/service/CouponService.java +++ b/src/main/java/coupon/service/CouponService.java @@ -12,8 +12,8 @@ public class CouponService { private final CouponRepository couponRepository; - public void create(Coupon coupon) { - couponRepository.save(coupon); + public Coupon create(Coupon coupon) { + return couponRepository.save(coupon); } @Transactional(readOnly = true) From 8a21b068be9b4071e3278c7df35b87ef6d0ca397 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 16:46:41 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=ED=95=A0=EC=9D=B8=EC=9C=A8=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/coupon/domain/coupon/Coupon.java | 12 +++++ .../java/coupon/domain/coupon/CouponTest.java | 52 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/test/java/coupon/domain/coupon/CouponTest.java diff --git a/src/main/java/coupon/domain/coupon/Coupon.java b/src/main/java/coupon/domain/coupon/Coupon.java index d33d71f4..d6e50180 100644 --- a/src/main/java/coupon/domain/coupon/Coupon.java +++ b/src/main/java/coupon/domain/coupon/Coupon.java @@ -15,6 +15,9 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Coupon { + private static final int MINIMUM_DISCOUNT_RATE = 3; + private static final int MAXIMUM_DISCOUNT_RATE = 20; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,10 +33,19 @@ public class Coupon { public Coupon(CouponName couponName, DiscountMount discountMount, MinimumMount minimumMount, Category category, Period period) { + validateDiscountRate(discountMount, minimumMount); this.couponName = couponName; this.discountMount = discountMount; this.minimumMount = minimumMount; this.category = category; this.period = period; } + + private void validateDiscountRate(DiscountMount discountMount, MinimumMount minimumMount) { + float discountRate = (float) discountMount.getDiscountMount() / minimumMount.getMinimumMount() * 100; + int truncatedDiscountRate = (int) discountRate; + if (truncatedDiscountRate < MINIMUM_DISCOUNT_RATE || truncatedDiscountRate > MAXIMUM_DISCOUNT_RATE) { + throw new IllegalArgumentException("쿠폰 할인율은 3% 이상, 20% 이하여야 합니다."); + } + } } diff --git a/src/test/java/coupon/domain/coupon/CouponTest.java b/src/test/java/coupon/domain/coupon/CouponTest.java new file mode 100644 index 00000000..a4b8d10e --- /dev/null +++ b/src/test/java/coupon/domain/coupon/CouponTest.java @@ -0,0 +1,52 @@ +package coupon.domain.coupon; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import coupon.domain.Category; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class CouponTest { + + @ParameterizedTest + @ValueSource(ints = {1000, 6000}) + @DisplayName("범위 내의 할인율로 생성할 수 있다.") + void discountRate(int discountMount) { + assertDoesNotThrow(() -> new Coupon( + new CouponName("총대마켓쿠폰"), + new DiscountMount(discountMount), + new MinimumMount(30000), + Category.FASHION, + new Period(LocalDate.now(), LocalDate.now().plusDays(1)))); + } + + @Test + @DisplayName("할인율 최소 할인율 미만일 경우 예외로 처리한다.") + void underMinimum() { + assertThatThrownBy(() -> new Coupon( + new CouponName("총대마켓쿠폰"), + new DiscountMount(1000), + new MinimumMount(50000), + Category.FASHION, + new Period(LocalDate.now(), LocalDate.now().plusDays(1)))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("쿠폰 할인율은 3% 이상, 20% 이하여야 합니다."); + } + + @Test + @DisplayName("할인율 최대 할인율 초과일 경우 예외로 처리한다.") + void overMaximum() { + assertThatThrownBy(() -> new Coupon( + new CouponName("총대마켓쿠폰"), + new DiscountMount(7000), + new MinimumMount(30000), + Category.FASHION, + new Period(LocalDate.now(), LocalDate.now().plusDays(1)))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("쿠폰 할인율은 3% 이상, 20% 이하여야 합니다."); + } +} From ea7c7489ab63952a8efe2c2f8ca3a1bbc85d26ff Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Fri, 18 Oct 2024 17:53:11 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EB=B3=B5=EC=A0=9C=20=EC=A7=80?= =?UTF-8?q?=EC=97=B0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/coupon/service/CouponService.java | 10 +++++++--- src/test/java/coupon/service/CouponServiceTest.java | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/coupon/service/CouponService.java b/src/main/java/coupon/service/CouponService.java index e2d497de..d2ed3ecd 100644 --- a/src/main/java/coupon/service/CouponService.java +++ b/src/main/java/coupon/service/CouponService.java @@ -2,21 +2,25 @@ import coupon.domain.coupon.Coupon; import coupon.repository.CouponRepository; -import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor public class CouponService { private final CouponRepository couponRepository; + public CouponService(CouponRepository couponRepository, ApplicationContext context) { + this.couponRepository = couponRepository; + } + + @Transactional public Coupon create(Coupon coupon) { return couponRepository.save(coupon); } - @Transactional(readOnly = true) + @Transactional public Coupon getCoupon(Long couponId) { return couponRepository.findById(couponId) .orElseThrow(() -> new IllegalStateException("존재하지 않는 쿠폰 id입니다.")); diff --git a/src/test/java/coupon/service/CouponServiceTest.java b/src/test/java/coupon/service/CouponServiceTest.java index f9775a23..7bf57cc5 100644 --- a/src/test/java/coupon/service/CouponServiceTest.java +++ b/src/test/java/coupon/service/CouponServiceTest.java @@ -1,5 +1,7 @@ package coupon.service; +import static org.assertj.core.api.Assertions.assertThat; + import coupon.domain.Category; import coupon.domain.coupon.Coupon; import coupon.domain.coupon.CouponName; @@ -18,14 +20,15 @@ class CouponServiceTest { private CouponService couponService; @Test - void create() { - Coupon coupon = new Coupon( + void 복제지연테스트() { + Coupon coupon = couponService.create(new Coupon( new CouponName("쿠폰"), new DiscountMount(1000), new MinimumMount(5000), Category.FASHION, new Period(LocalDate.now().minusDays(1), LocalDate.now().plusDays(1)) - ); - couponService.create(coupon); + )); + Coupon savedCoupon = couponService.getCoupon(coupon.getId()); + assertThat(savedCoupon).isNotNull(); } } From 2372b1d90a07cfbea2511db78e858ba33f7ddc92 Mon Sep 17 00:00:00 2001 From: slimsha2dy Date: Sat, 19 Oct 2024 19:24:24 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20write=20=EC=9E=AC=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/coupon/service/CouponService.java | 15 +++++++++++---- .../coupon/service/support/DataSourceSupport.java | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 src/main/java/coupon/service/support/DataSourceSupport.java diff --git a/src/main/java/coupon/service/CouponService.java b/src/main/java/coupon/service/CouponService.java index d2ed3ecd..31a8e3f1 100644 --- a/src/main/java/coupon/service/CouponService.java +++ b/src/main/java/coupon/service/CouponService.java @@ -2,7 +2,7 @@ import coupon.domain.coupon.Coupon; import coupon.repository.CouponRepository; -import org.springframework.context.ApplicationContext; +import coupon.service.support.DataSourceSupport; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,9 +10,11 @@ public class CouponService { private final CouponRepository couponRepository; + private final DataSourceSupport dataSourceSupport; - public CouponService(CouponRepository couponRepository, ApplicationContext context) { + public CouponService(CouponRepository couponRepository, DataSourceSupport dataSourceSupport) { this.couponRepository = couponRepository; + this.dataSourceSupport = dataSourceSupport; } @Transactional @@ -20,9 +22,14 @@ public Coupon create(Coupon coupon) { return couponRepository.save(coupon); } - @Transactional + @Transactional(readOnly = true) public Coupon getCoupon(Long couponId) { return couponRepository.findById(couponId) - .orElseThrow(() -> new IllegalStateException("존재하지 않는 쿠폰 id입니다.")); + .orElse(getCouponFromWriter(couponId)); + } + + private Coupon getCouponFromWriter(Long couponId) { + return dataSourceSupport.executeOnWriter(() -> couponRepository.findById(couponId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 쿠폰 id입니다."))); } } diff --git a/src/main/java/coupon/service/support/DataSourceSupport.java b/src/main/java/coupon/service/support/DataSourceSupport.java new file mode 100644 index 00000000..306b8cee --- /dev/null +++ b/src/main/java/coupon/service/support/DataSourceSupport.java @@ -0,0 +1,15 @@ +package coupon.service.support; + +import java.util.function.Supplier; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class DataSourceSupport { + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public T executeOnWriter(Supplier supplier) { + return supplier.get(); + } +}