From e900a4a32f28333119f3d05fe236f2845c9ba2b4 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Mon, 15 Dec 2025 21:32:07 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20CoverImage=20->=20ImageType=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/CoverImage.java | 7 +++++ .../nextstep/courses/domain/ImageType.java | 30 +++++++++++++++++++ .../courses/domain/ImageTypeTest.java | 28 +++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/CoverImage.java create mode 100644 src/main/java/nextstep/courses/domain/ImageType.java create mode 100644 src/test/java/nextstep/courses/domain/ImageTypeTest.java diff --git a/src/main/java/nextstep/courses/domain/CoverImage.java b/src/main/java/nextstep/courses/domain/CoverImage.java new file mode 100644 index 000000000..0a3bc2a4f --- /dev/null +++ b/src/main/java/nextstep/courses/domain/CoverImage.java @@ -0,0 +1,7 @@ +package nextstep.courses.domain; + +public class CoverImage { + + private int imageSize; + private ImageType imageType; +} diff --git a/src/main/java/nextstep/courses/domain/ImageType.java b/src/main/java/nextstep/courses/domain/ImageType.java new file mode 100644 index 000000000..a8420c326 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/ImageType.java @@ -0,0 +1,30 @@ +package nextstep.courses.domain; + +import java.util.Arrays; + +public enum ImageType { + + GIF("gif"), + JPG("jpg", "jpeg"), + PNG("png"), + SVG("svg"); + + private final String[] extensions; + + ImageType(String... extensions) { + this.extensions = extensions; + } + + private boolean matches(String extension) { + return Arrays.stream(extensions) + .anyMatch(ext -> ext.equalsIgnoreCase(extension)); + } + + public static ImageType from(String extension) { + return Arrays.stream(values()) + .filter(type -> type.matches(extension)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException()); + } + +} diff --git a/src/test/java/nextstep/courses/domain/ImageTypeTest.java b/src/test/java/nextstep/courses/domain/ImageTypeTest.java new file mode 100644 index 000000000..080d0384e --- /dev/null +++ b/src/test/java/nextstep/courses/domain/ImageTypeTest.java @@ -0,0 +1,28 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ImageTypeTest { + + @Test + @DisplayName("유효한 확장자") + void from_valid_extension() { + assertThat(ImageType.from("gif")).isEqualTo(ImageType.GIF); + assertThat(ImageType.from("GIF")).isEqualTo(ImageType.GIF); + assertThat(ImageType.from("jpg")).isEqualTo(ImageType.JPG); + assertThat(ImageType.from("jpeg")).isEqualTo(ImageType.JPG); + } + + @Test + @DisplayName("유효하지 않은 확장자") + void from_invalid_extension() { + assertThatThrownBy(() -> ImageType.from("txt")) + .isInstanceOf(IllegalArgumentException.class); + } + + +} \ No newline at end of file From 82714114da0306345657350193636171bdc180d8 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Mon, 15 Dec 2025 22:45:47 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20CoverImage=20->=20ImageSize=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/CoverImage.java | 5 ++-- .../nextstep/courses/domain/ImageSize.java | 29 ++++++++++++++++++ .../courses/domain/ImageSizeTest.java | 30 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/ImageSize.java create mode 100644 src/test/java/nextstep/courses/domain/ImageSizeTest.java diff --git a/src/main/java/nextstep/courses/domain/CoverImage.java b/src/main/java/nextstep/courses/domain/CoverImage.java index 0a3bc2a4f..ccfab3aef 100644 --- a/src/main/java/nextstep/courses/domain/CoverImage.java +++ b/src/main/java/nextstep/courses/domain/CoverImage.java @@ -1,7 +1,8 @@ package nextstep.courses.domain; public class CoverImage { - - private int imageSize; + private ImageSize imageSize; private ImageType imageType; + private int width; + private int height; } diff --git a/src/main/java/nextstep/courses/domain/ImageSize.java b/src/main/java/nextstep/courses/domain/ImageSize.java new file mode 100644 index 000000000..b03838043 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/ImageSize.java @@ -0,0 +1,29 @@ +package nextstep.courses.domain; + +import java.util.Objects; + +public class ImageSize { + + public static final int MAX_SIZE = 1_048_576; + private int imageSize; + + public ImageSize(int imageSize) { + if (imageSize > MAX_SIZE) { + throw new IllegalArgumentException(); + } + this.imageSize = imageSize; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + ImageSize imageSize1 = (ImageSize) object; + return imageSize == imageSize1.imageSize; + } + + @Override + public int hashCode() { + return Objects.hashCode(imageSize); + } +} diff --git a/src/test/java/nextstep/courses/domain/ImageSizeTest.java b/src/test/java/nextstep/courses/domain/ImageSizeTest.java new file mode 100644 index 000000000..5685a14cd --- /dev/null +++ b/src/test/java/nextstep/courses/domain/ImageSizeTest.java @@ -0,0 +1,30 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static nextstep.courses.domain.ImageSize.MAX_SIZE; +import static org.assertj.core.api.Assertions.*; + +class ImageSizeTest { + + @Test + @DisplayName("1MB 이하의 이미지 크기는 생성된다") + void create_image_size() { + // 방법1 + ImageSize imageSize = new ImageSize(MAX_SIZE); + + assertThat(imageSize).isEqualTo(new ImageSize(MAX_SIZE)); + + // 방법2 + assertThatNoException() + .isThrownBy(() -> new ImageSize(MAX_SIZE)); + } + + @Test + @DisplayName("1MB보다 큰 이미지 크기는 예외가 발생한다") + void create_image_size_max_exception() { + assertThatThrownBy(() -> new ImageSize(MAX_SIZE + 1)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file From cdcc5a9bf70d7bc76e2c349af27ae5b2ce26bb72 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Mon, 15 Dec 2025 23:12:42 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20CoverImage=20->=20ImageDimension?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/CoverImage.java | 3 +- .../courses/domain/ImageDimension.java | 30 ++++++++++++++ .../courses/domain/ImageDimensionTest.java | 41 +++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/ImageDimension.java create mode 100644 src/test/java/nextstep/courses/domain/ImageDimensionTest.java diff --git a/src/main/java/nextstep/courses/domain/CoverImage.java b/src/main/java/nextstep/courses/domain/CoverImage.java index ccfab3aef..74afc0b80 100644 --- a/src/main/java/nextstep/courses/domain/CoverImage.java +++ b/src/main/java/nextstep/courses/domain/CoverImage.java @@ -3,6 +3,5 @@ public class CoverImage { private ImageSize imageSize; private ImageType imageType; - private int width; - private int height; + private ImageDimension imageDimension; } diff --git a/src/main/java/nextstep/courses/domain/ImageDimension.java b/src/main/java/nextstep/courses/domain/ImageDimension.java new file mode 100644 index 000000000..806de0874 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/ImageDimension.java @@ -0,0 +1,30 @@ +package nextstep.courses.domain; + +public class ImageDimension { + + public static final int HEIGHT_RATIO = 2; + public static final int WIDTH_RATIO = 3; + public static final int MIN_WIDTH = 300; + public static final int MIN_HEIGHT = 200; + private int width; + private int height; + + public ImageDimension(int width, int height) { + validateMinimumSize(width, height); + validateRatio(width, height); + this.width = width; + this.height = height; + } + + private static void validateMinimumSize(int width, int height) { + if (width < MIN_WIDTH || height < MIN_HEIGHT) { + throw new IllegalArgumentException(); + } + } + + private static void validateRatio(int width, int height) { + if (!(HEIGHT_RATIO * width == WIDTH_RATIO * height)) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/test/java/nextstep/courses/domain/ImageDimensionTest.java b/src/test/java/nextstep/courses/domain/ImageDimensionTest.java new file mode 100644 index 000000000..1f3805d39 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/ImageDimensionTest.java @@ -0,0 +1,41 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ImageDimensionTest { + + @Test + @DisplayName("width가 300px 미만이면 예외가 발생한다") + void create_width_exception() { + assertThatThrownBy(() -> new ImageDimension(299, 200)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("height가 200px 미만이면 예외가 발생한다") + void create_height_exception() { + assertThatThrownBy(() -> new ImageDimension(300, 199)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("width와 height는 3:2 비율이 아니면 예외가 발생한다") + void create_width_height_exception() { + assertThatThrownBy(() -> new ImageDimension(300, 300)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("width_height_정상생성") + void create_success() { + assertThatNoException() + .isThrownBy(() -> new ImageDimension(300, 200)); + + assertThatNoException() + .isThrownBy(() -> new ImageDimension(600, 400)); + } +} \ No newline at end of file From 20eb67f5eb0fcd365f7cf04ec61ee94f9a1a4fd2 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Mon, 15 Dec 2025 23:26:55 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20Session=20->=20SessionDuration(?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=EC=9D=BC=EC=9E=90/=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=EC=9D=BC=EC=9E=90)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/courses/domain/Session.java | 8 +++++ .../courses/domain/SessionDuration.java | 20 ++++++++++++ .../courses/domain/SessionDurationTest.java | 31 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/Session.java create mode 100644 src/main/java/nextstep/courses/domain/SessionDuration.java create mode 100644 src/test/java/nextstep/courses/domain/SessionDurationTest.java diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java new file mode 100644 index 000000000..c31afbfaf --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -0,0 +1,8 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class Session { + private SessionDuration sessionDuration; + private CoverImage coverImage; +} diff --git a/src/main/java/nextstep/courses/domain/SessionDuration.java b/src/main/java/nextstep/courses/domain/SessionDuration.java new file mode 100644 index 000000000..54d7714a2 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/SessionDuration.java @@ -0,0 +1,20 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class SessionDuration { + private LocalDateTime startDate; + private LocalDateTime endDate; + + public SessionDuration(LocalDateTime startDate, LocalDateTime endDate) { + validateDate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validateDate(LocalDateTime startDate, LocalDateTime endDate) { + if (!startDate.isBefore(endDate)) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/test/java/nextstep/courses/domain/SessionDurationTest.java b/src/test/java/nextstep/courses/domain/SessionDurationTest.java new file mode 100644 index 000000000..fbc26915f --- /dev/null +++ b/src/test/java/nextstep/courses/domain/SessionDurationTest.java @@ -0,0 +1,31 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SessionDurationTest { + + @Test + @DisplayName("시작일과 종료일이 같으면 exception") + void start_equals_end_exception() { + LocalDateTime start = LocalDateTime.of(2025, 12, 15, 00, 00); + LocalDateTime end = LocalDateTime.of(2025, 12, 15, 00, 00); + + assertThatThrownBy(() -> new SessionDuration(start, end)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("시작일이 종료일보다 크면 exception") + void start_after_end_exception() { + LocalDateTime start = LocalDateTime.of(2025, 12, 15, 14, 00); + LocalDateTime end = LocalDateTime.of(2025, 12, 15, 13, 00); + + assertThatThrownBy(() -> new SessionDuration(start, end)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file From 2ae44f90c1513300efb5120e266febfa69d000a5 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Tue, 16 Dec 2025 21:58:07 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20Session=20->=20SessionState=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/SessionState.java | 13 ++++++++++ .../courses/domain/SessionStateTest.java | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/SessionState.java create mode 100644 src/test/java/nextstep/courses/domain/SessionStateTest.java diff --git a/src/main/java/nextstep/courses/domain/SessionState.java b/src/main/java/nextstep/courses/domain/SessionState.java new file mode 100644 index 000000000..e087309d5 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/SessionState.java @@ -0,0 +1,13 @@ +package nextstep.courses.domain; + +public enum SessionState { + READY, + OPEN, + CLOSED; + + public void validateEnroll() { + if (this != OPEN) { + throw new IllegalStateException(); + } + } +} diff --git a/src/test/java/nextstep/courses/domain/SessionStateTest.java b/src/test/java/nextstep/courses/domain/SessionStateTest.java new file mode 100644 index 000000000..85ac02fc3 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/SessionStateTest.java @@ -0,0 +1,26 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SessionStateTest { + @Test + @DisplayName("OPEN은 등록이 가능하다") + void enroll_success() { + assertThatCode(() -> SessionState.OPEN.validateEnroll()) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("READY나 CLOSED는 등록할 수 없다") + void enroll_fail() { + assertThatThrownBy(() -> SessionState.READY.validateEnroll()) + .isInstanceOf(IllegalStateException.class); + + assertThatThrownBy(() -> SessionState.CLOSED.validateEnroll()) + .isInstanceOf(IllegalStateException.class); + } +} \ No newline at end of file From 31bccd9f085435969f5685f91cbb0f4c2427c260 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Tue, 16 Dec 2025 22:09:54 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20Session=20->=20EnrollmentPolicy?= =?UTF-8?q?=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20->=20FreeEnrollmentPolicy(=EB=AC=B4=EB=A3=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/courses/domain/EnrollmentPolicy.java | 7 +++++++ .../nextstep/courses/domain/FreeEnrollmentPolicy.java | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/EnrollmentPolicy.java create mode 100644 src/main/java/nextstep/courses/domain/FreeEnrollmentPolicy.java diff --git a/src/main/java/nextstep/courses/domain/EnrollmentPolicy.java b/src/main/java/nextstep/courses/domain/EnrollmentPolicy.java new file mode 100644 index 000000000..ee8b0773e --- /dev/null +++ b/src/main/java/nextstep/courses/domain/EnrollmentPolicy.java @@ -0,0 +1,7 @@ +package nextstep.courses.domain; + +import nextstep.payments.domain.Payment; + +public interface EnrollmentPolicy { + void validateEnrollment(Payment payment); +} diff --git a/src/main/java/nextstep/courses/domain/FreeEnrollmentPolicy.java b/src/main/java/nextstep/courses/domain/FreeEnrollmentPolicy.java new file mode 100644 index 000000000..21720b9ec --- /dev/null +++ b/src/main/java/nextstep/courses/domain/FreeEnrollmentPolicy.java @@ -0,0 +1,10 @@ +package nextstep.courses.domain; + +import nextstep.payments.domain.Payment; + +public class FreeEnrollmentPolicy implements EnrollmentPolicy{ + @Override + public void validateEnrollment(Payment payment) { + // 무료강의는 검증하지 않는다 + } +} From 7131ea44c52fdfbe7e681642049aa0978389f655 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Tue, 16 Dec 2025 22:11:15 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20Session=20->=20EnrollmentPolicy?= =?UTF-8?q?=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20->=20PaidEnrollmentPolicy(=EC=9C=A0=EB=A3=8C)=20->?= =?UTF-8?q?=20Money,=20Capacity=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20Payment=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/Capacity.java | 18 ++++++++++++ .../java/nextstep/courses/domain/Money.java | 28 +++++++++++++++++++ .../courses/domain/PaidEnrollmentPolicy.java | 26 +++++++++++++++++ .../nextstep/payments/domain/Payment.java | 10 +++++-- .../nextstep/courses/domain/CapacityTest.java | 17 +++++++++++ .../nextstep/courses/domain/MoneyTest.java | 17 +++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/Capacity.java create mode 100644 src/main/java/nextstep/courses/domain/Money.java create mode 100644 src/main/java/nextstep/courses/domain/PaidEnrollmentPolicy.java create mode 100644 src/test/java/nextstep/courses/domain/CapacityTest.java create mode 100644 src/test/java/nextstep/courses/domain/MoneyTest.java diff --git a/src/main/java/nextstep/courses/domain/Capacity.java b/src/main/java/nextstep/courses/domain/Capacity.java new file mode 100644 index 000000000..8153d51d6 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Capacity.java @@ -0,0 +1,18 @@ +package nextstep.courses.domain; + +public class Capacity { + + private int max; + private int current; + + public Capacity(int max, int current) { + this.max = max; + this.current = current; + } + + public void validateAvailable() { + if (current > max) { + throw new IllegalStateException(); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/Money.java b/src/main/java/nextstep/courses/domain/Money.java new file mode 100644 index 000000000..bd571b378 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Money.java @@ -0,0 +1,28 @@ +package nextstep.courses.domain; + +import java.util.Objects; + +public class Money { + private final long amount; + + public Money(long amount) { + this.amount = amount; + } + + public boolean isEqualTo(Money other) { + return this.equals(other); + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + Money money = (Money) object; + return amount == money.amount; + } + + @Override + public int hashCode() { + return Objects.hashCode(amount); + } +} diff --git a/src/main/java/nextstep/courses/domain/PaidEnrollmentPolicy.java b/src/main/java/nextstep/courses/domain/PaidEnrollmentPolicy.java new file mode 100644 index 000000000..f471b5a3f --- /dev/null +++ b/src/main/java/nextstep/courses/domain/PaidEnrollmentPolicy.java @@ -0,0 +1,26 @@ +package nextstep.courses.domain; + +import nextstep.payments.domain.Payment; + +public class PaidEnrollmentPolicy implements EnrollmentPolicy { + + private final Money money; + private final Capacity capacity; + + public PaidEnrollmentPolicy(Money money, Capacity capacity) { + this.money = money; + this.capacity = capacity; + } + + @Override + public void validateEnrollment(Payment payment) { + capacity.validateAvailable(); + validatePayment(payment); + } + + private void validatePayment(Payment payment) { + if (!payment.isSameAmount(money)) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 57d833f85..6d4c2b720 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -1,5 +1,7 @@ package nextstep.payments.domain; +import nextstep.courses.domain.Money; + import java.time.LocalDateTime; public class Payment { @@ -12,7 +14,7 @@ public class Payment { private Long nsUserId; // 결제 금액 - private Long amount; + private Money amount; private LocalDateTime createdAt; @@ -23,7 +25,11 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.id = id; this.sessionId = sessionId; this.nsUserId = nsUserId; - this.amount = amount; + this.amount = new Money(amount); this.createdAt = LocalDateTime.now(); } + + public boolean isSameAmount(Money money) { + return amount.isEqualTo(money); + } } diff --git a/src/test/java/nextstep/courses/domain/CapacityTest.java b/src/test/java/nextstep/courses/domain/CapacityTest.java new file mode 100644 index 000000000..38d3806c0 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/CapacityTest.java @@ -0,0 +1,17 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CapacityTest { + + @Test + @DisplayName("최대 인원을 넘어서 수강신청이 들어오면 Exceptionn") + void max() { + Capacity capacity = new Capacity(300, 301); + assertThatThrownBy(() -> capacity.validateAvailable()) + .isInstanceOf(IllegalStateException.class); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/MoneyTest.java b/src/test/java/nextstep/courses/domain/MoneyTest.java new file mode 100644 index 000000000..361ef9858 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/MoneyTest.java @@ -0,0 +1,17 @@ +package nextstep.courses.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class MoneyTest { + + @Test + @DisplayName("금액이 같으면, true") + void isEqualTo() { + Money money = new Money(5000); + assertThat(money.isEqualTo(new Money(5000))).isTrue(); + } +} \ No newline at end of file From 00c225778314b287c6e03e08197e09e9d0ba9c0a Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Tue, 16 Dec 2025 22:11:36 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20Session=20=ED=95=84=EB=93=9C=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 --- src/main/java/nextstep/courses/domain/Session.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index c31afbfaf..87a1ec45a 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -1,8 +1,8 @@ package nextstep.courses.domain; -import java.time.LocalDateTime; - public class Session { private SessionDuration sessionDuration; private CoverImage coverImage; + private EnrollmentPolicy enrollmentPolicy; + private SessionState sessionState; } From 3612e77e6078ff6cee9d5d3adaf3f5818c4fd958 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Tue, 16 Dec 2025 22:58:16 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20Session=20->=20CoverImage=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(Session=20=EA=B5=AC=ED=98=84=20=EC=A0=84,?= =?UTF-8?q?=20=EB=B9=A0=EC=A7=84=20=EB=B6=80=EB=B6=84=20=EC=A0=90=EA=B2=80?= =?UTF-8?q?..)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/courses/domain/CoverImage.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/nextstep/courses/domain/CoverImage.java b/src/main/java/nextstep/courses/domain/CoverImage.java index 74afc0b80..6c677c07e 100644 --- a/src/main/java/nextstep/courses/domain/CoverImage.java +++ b/src/main/java/nextstep/courses/domain/CoverImage.java @@ -4,4 +4,14 @@ public class CoverImage { private ImageSize imageSize; private ImageType imageType; private ImageDimension imageDimension; + + public CoverImage(int size, ImageType imageType, int width, int height) { + this(new ImageSize(size), imageType, new ImageDimension(width, height)); + } + + public CoverImage(ImageSize imageSize, ImageType imageType, ImageDimension imageDimension) { + this.imageSize = imageSize; + this.imageType = imageType; + this.imageDimension = imageDimension; + } } From b9e6e9701001f9d06e92cd64f49f16e2325510f9 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Tue, 16 Dec 2025 23:38:19 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20Session=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/courses/domain/Session.java | 38 +++++++++++++++++-- .../nextstep/courses/domain/SessionTest.java | 13 +++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/test/java/nextstep/courses/domain/SessionTest.java diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index 87a1ec45a..584e59ec6 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -1,8 +1,38 @@ package nextstep.courses.domain; +import nextstep.payments.domain.Payment; + +import java.time.LocalDateTime; + public class Session { - private SessionDuration sessionDuration; - private CoverImage coverImage; - private EnrollmentPolicy enrollmentPolicy; - private SessionState sessionState; + private final long id; + private final SessionDuration sessionDuration; + private final CoverImage coverImage; + private final EnrollmentPolicy enrollmentPolicy; + private final SessionState sessionState; + + public Session(long id + , LocalDateTime startDate + , LocalDateTime endDate + , int size + , ImageType imageType + , int width + , int height + , EnrollmentPolicy enrollmentPolicy + , SessionState sessionState) { + this(id, new SessionDuration(startDate, endDate), new CoverImage(size, imageType, width, height) + , enrollmentPolicy, sessionState); + } + + public Session(long id, SessionDuration sessionDuration, CoverImage coverImage + , EnrollmentPolicy enrollmentPolicy, SessionState sessionState) { + this.id = id; + this.sessionDuration = sessionDuration; + this.coverImage = coverImage; + this.enrollmentPolicy = enrollmentPolicy; + this.sessionState = sessionState; + } + + public void enroll() { + } } diff --git a/src/test/java/nextstep/courses/domain/SessionTest.java b/src/test/java/nextstep/courses/domain/SessionTest.java new file mode 100644 index 000000000..a14c62e3d --- /dev/null +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -0,0 +1,13 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SessionTest { + + @Test + void enroll() { + + } +} \ No newline at end of file From 770df4d33e2b3f7f5b1af7981773a90e31fdce40 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Wed, 17 Dec 2025 22:42:29 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20CoverImage=20->=20ImageType?= =?UTF-8?q?=EC=97=90=20fileName=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/CoverImage.java | 6 +++--- .../java/nextstep/courses/domain/ImageType.java | 15 ++++++++++++++- .../java/nextstep/courses/domain/Session.java | 4 ++-- .../nextstep/courses/domain/ImageTypeTest.java | 17 +++++++++++------ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/main/java/nextstep/courses/domain/CoverImage.java b/src/main/java/nextstep/courses/domain/CoverImage.java index 6c677c07e..e95702300 100644 --- a/src/main/java/nextstep/courses/domain/CoverImage.java +++ b/src/main/java/nextstep/courses/domain/CoverImage.java @@ -5,11 +5,11 @@ public class CoverImage { private ImageType imageType; private ImageDimension imageDimension; - public CoverImage(int size, ImageType imageType, int width, int height) { - this(new ImageSize(size), imageType, new ImageDimension(width, height)); + public CoverImage(int size, String fileName, int width, int height) { + this(new ImageSize(size), ImageType.fromFileName(fileName), new ImageDimension(width, height)); } - public CoverImage(ImageSize imageSize, ImageType imageType, ImageDimension imageDimension) { + private CoverImage(ImageSize imageSize, ImageType imageType, ImageDimension imageDimension) { this.imageSize = imageSize; this.imageType = imageType; this.imageDimension = imageDimension; diff --git a/src/main/java/nextstep/courses/domain/ImageType.java b/src/main/java/nextstep/courses/domain/ImageType.java index a8420c326..acaac0063 100644 --- a/src/main/java/nextstep/courses/domain/ImageType.java +++ b/src/main/java/nextstep/courses/domain/ImageType.java @@ -20,11 +20,24 @@ private boolean matches(String extension) { .anyMatch(ext -> ext.equalsIgnoreCase(extension)); } - public static ImageType from(String extension) { + private static ImageType from(String extension) { return Arrays.stream(values()) .filter(type -> type.matches(extension)) .findFirst() .orElseThrow(() -> new IllegalArgumentException()); } + public static ImageType fromFileName(String fileName) { + String extension = extractExtension(fileName); + return from(extension); + } + + private static String extractExtension(String fileName) { + int index = fileName.lastIndexOf("."); + if (index == -1 || index == fileName.length() - 1) { + throw new IllegalArgumentException(); + } + return fileName.substring(index + 1); + } + } diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index 584e59ec6..5aac5a1c0 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -15,12 +15,12 @@ public Session(long id , LocalDateTime startDate , LocalDateTime endDate , int size - , ImageType imageType + , String fileName , int width , int height , EnrollmentPolicy enrollmentPolicy , SessionState sessionState) { - this(id, new SessionDuration(startDate, endDate), new CoverImage(size, imageType, width, height) + this(id, new SessionDuration(startDate, endDate), new CoverImage(size, fileName, width, height) , enrollmentPolicy, sessionState); } diff --git a/src/test/java/nextstep/courses/domain/ImageTypeTest.java b/src/test/java/nextstep/courses/domain/ImageTypeTest.java index 080d0384e..d3e2f7da2 100644 --- a/src/test/java/nextstep/courses/domain/ImageTypeTest.java +++ b/src/test/java/nextstep/courses/domain/ImageTypeTest.java @@ -11,18 +11,23 @@ class ImageTypeTest { @Test @DisplayName("유효한 확장자") void from_valid_extension() { - assertThat(ImageType.from("gif")).isEqualTo(ImageType.GIF); - assertThat(ImageType.from("GIF")).isEqualTo(ImageType.GIF); - assertThat(ImageType.from("jpg")).isEqualTo(ImageType.JPG); - assertThat(ImageType.from("jpeg")).isEqualTo(ImageType.JPG); + assertThat(ImageType.fromFileName("test.gif")).isEqualTo(ImageType.GIF); + assertThat(ImageType.fromFileName("test.GIF")).isEqualTo(ImageType.GIF); + assertThat(ImageType.fromFileName("test.jpg")).isEqualTo(ImageType.JPG); + assertThat(ImageType.fromFileName("test.jpeg")).isEqualTo(ImageType.JPG); } @Test @DisplayName("유효하지 않은 확장자") void from_invalid_extension() { - assertThatThrownBy(() -> ImageType.from("txt")) + assertThatThrownBy(() -> ImageType.fromFileName("test.txt")) .isInstanceOf(IllegalArgumentException.class); } - + @Test + @DisplayName("확장자가 없는 이름은 유효하지 않다") + void fromFileName_invalid_fileName() { + assertThatThrownBy(() -> ImageType.fromFileName("test")) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file From 7dd2e8c62e48ac5d1e521cdc10a68ea0b8c093a9 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Thu, 18 Dec 2025 02:05:03 +0900 Subject: [PATCH 12/14] =?UTF-8?q?test:=20PaidEnrollmentPolicy=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/PaidEnrollmentPolicyTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/test/java/nextstep/courses/domain/PaidEnrollmentPolicyTest.java diff --git a/src/test/java/nextstep/courses/domain/PaidEnrollmentPolicyTest.java b/src/test/java/nextstep/courses/domain/PaidEnrollmentPolicyTest.java new file mode 100644 index 000000000..ae8a4d15a --- /dev/null +++ b/src/test/java/nextstep/courses/domain/PaidEnrollmentPolicyTest.java @@ -0,0 +1,50 @@ +package nextstep.courses.domain; + +import nextstep.payments.domain.Payment; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PaidEnrollmentPolicyTest { + + @Test + @DisplayName("유료강의 - 성공") + void validateEnrollment_success() { + Money price = new Money(5000); + Capacity capacity = new Capacity(30, 10); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(price, capacity); + + Payment payment = new Payment("p1", 1L, 1L, 5_000L); + + assertThatCode(() -> policy.validateEnrollment(payment)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("유료강의 - 수강생 초과 실패") + void validateEnrollment_fail_capacity() { + Money price = new Money(5000); + Capacity capacity = new Capacity(30, 31); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(price, capacity); + + Payment payment = new Payment("p1", 1L, 1L, 5_000L); + + assertThatThrownBy(() -> policy.validateEnrollment(payment)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + @DisplayName("유료강의 - 금액 불일치 실패") + void validateEnrollment_fail_amount() { + Money price = new Money(5000); + Capacity capacity = new Capacity(30, 20); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(price, capacity); + + Payment payment = new Payment("p1", 1L, 1L, 4_000L); + + assertThatThrownBy(() -> policy.validateEnrollment(payment)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file From bfc3ef3bbc7bb4cdf76088e702838c8cbe57ff32 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Thu, 18 Dec 2025 02:58:32 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20Session=20-=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/courses/domain/Capacity.java | 2 +- .../nextstep/courses/domain/Enrollment.java | 15 +++++++ .../java/nextstep/courses/domain/Session.java | 5 ++- .../nextstep/courses/domain/SessionTest.java | 42 ++++++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/Enrollment.java diff --git a/src/main/java/nextstep/courses/domain/Capacity.java b/src/main/java/nextstep/courses/domain/Capacity.java index 8153d51d6..b1ab22e49 100644 --- a/src/main/java/nextstep/courses/domain/Capacity.java +++ b/src/main/java/nextstep/courses/domain/Capacity.java @@ -2,7 +2,7 @@ public class Capacity { - private int max; + private final int max; private int current; public Capacity(int max, int current) { diff --git a/src/main/java/nextstep/courses/domain/Enrollment.java b/src/main/java/nextstep/courses/domain/Enrollment.java new file mode 100644 index 000000000..e922ba2eb --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Enrollment.java @@ -0,0 +1,15 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class Enrollment { + private final Long sessionId; + private final Long userId; + private final LocalDateTime enrollmentDate; + + public Enrollment(Long sessionId, Long userId) { + this.sessionId = sessionId; + this.userId = userId; + this.enrollmentDate = LocalDateTime.now(); + } +} diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index 5aac5a1c0..462f1064d 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -33,6 +33,9 @@ public Session(long id, SessionDuration sessionDuration, CoverImage coverImage this.sessionState = sessionState; } - public void enroll() { + public Enrollment enroll(Long userId, Payment payment) { + sessionState.validateEnroll(); + enrollmentPolicy.validateEnrollment(payment); + return new Enrollment(this.id, userId); } } diff --git a/src/test/java/nextstep/courses/domain/SessionTest.java b/src/test/java/nextstep/courses/domain/SessionTest.java index a14c62e3d..97c84e346 100644 --- a/src/test/java/nextstep/courses/domain/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -1,13 +1,51 @@ package nextstep.courses.domain; +import nextstep.payments.domain.Payment; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class SessionTest { @Test - void enroll() { + @DisplayName("수강신청 - 성공") + void enroll_success() { + Long sessionId = 1L; + Long userId = 100L; + SessionState sessionState = SessionState.OPEN; + Money price = new Money(5000); + Capacity capacity = new Capacity(30, 20); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(price, capacity); + + Payment payment = new Payment("p1", sessionId, userId, 5000L); + + Session session = new Session(sessionId, LocalDateTime.now(), LocalDateTime.now().plusDays(7) + , 500_000, "test.jpg", 300, 200, policy, sessionState); + + assertThatCode(() -> session.enroll(userId, payment)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("수강신청 - 실패(상태:종료)") + void enroll_fail_closed() { + Long sessionId = 1L; + Long userId = 100L; + SessionState sessionState = SessionState.CLOSED; + Money price = new Money(5000); + Capacity capacity = new Capacity(30, 20); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(price, capacity); + + Payment payment = new Payment("p1", sessionId, userId, 5000L); + + Session session = new Session(sessionId, LocalDateTime.now(), LocalDateTime.now().plusDays(7) + , 500_000, "test.jpg", 300, 200, policy, sessionState); + assertThatThrownBy(() -> session.enroll(userId, payment)) + .isInstanceOf(IllegalStateException.class); } } \ No newline at end of file From 64113eb916c2d98327276ba7165c137f146f9b65 Mon Sep 17 00:00:00 2001 From: qwer920414-ctrl Date: Thu, 18 Dec 2025 03:07:55 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20Course=20-=20Session=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(List=20->=20Sessions=20=EC=9D=BC=EA=B8=89?= =?UTF-8?q?=EC=BB=AC=EB=A0=89=EC=85=98=20=EC=83=9D=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/courses/domain/Course.java | 14 ++++++++----- .../nextstep/courses/domain/Sessions.java | 20 +++++++++++++++++++ .../infrastructure/JdbcCourseRepository.java | 1 + 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/Sessions.java diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/Course.java index 0f6971604..f664a63a8 100644 --- a/src/main/java/nextstep/courses/domain/Course.java +++ b/src/main/java/nextstep/courses/domain/Course.java @@ -9,25 +9,29 @@ public class Course { private Long creatorId; + private final Sessions sessions; + private LocalDateTime createdAt; private LocalDateTime updatedAt; - public Course() { - } - public Course(String title, Long creatorId) { - this(0L, title, creatorId, LocalDateTime.now(), null); + this(0L, title, creatorId, new Sessions(), LocalDateTime.now(), null); } - public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Course(Long id, String title, Long creatorId, Sessions sessions, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.title = title; this.creatorId = creatorId; + this.sessions = sessions; this.createdAt = createdAt; this.updatedAt = updatedAt; } + public void addSession(Session session) { + sessions.add(session); + } + public String getTitle() { return title; } diff --git a/src/main/java/nextstep/courses/domain/Sessions.java b/src/main/java/nextstep/courses/domain/Sessions.java new file mode 100644 index 000000000..f8c04a774 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Sessions.java @@ -0,0 +1,20 @@ +package nextstep.courses.domain; + +import java.util.ArrayList; +import java.util.List; + +public class Sessions { + private final List sessions; + + public Sessions() { + this(new ArrayList<>()); + } + + public Sessions(List sessions) { + this.sessions = new ArrayList<>(sessions); + } + + public void add(Session session) { + this.sessions.add(session); + } +} diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java index f9122cbe3..44fedde4c 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java @@ -30,6 +30,7 @@ public Course findById(Long id) { rs.getLong(1), rs.getString(2), rs.getLong(3), + null, toLocalDateTime(rs.getTimestamp(4)), toLocalDateTime(rs.getTimestamp(5))); return jdbcTemplate.queryForObject(sql, rowMapper, id);