From f89b43c217596106de60148d1d6e9c0f949a0e65 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Fri, 15 Sep 2023 08:31:05 +0900 Subject: [PATCH 01/40] feat: add MailMessage --- .../kotlin/apply/domain/mail/MailMessage.kt | 56 +++++++++++++++++++ .../apply/domain/mail/MailReservation.kt | 3 + .../kotlin/apply/MailHistoryFixtures.kt.kt | 25 +++++++++ .../apply/domain/mail/MailMessageTest.kt | 25 +++++++++ 4 files changed, 109 insertions(+) create mode 100644 src/main/kotlin/apply/domain/mail/MailMessage.kt create mode 100644 src/main/kotlin/apply/domain/mail/MailReservation.kt create mode 100644 src/test/kotlin/apply/domain/mail/MailMessageTest.kt diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt new file mode 100644 index 000000000..95d149052 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -0,0 +1,56 @@ +package apply.domain.mail + +import java.time.LocalDateTime + +class MailMessage private constructor( + val subject: String, + val body: String, + val sender: String, + val recipients: List, + val creatorId: Long, + val createdDateTime: LocalDateTime = LocalDateTime.now(), + modifiedDateTime: LocalDateTime = LocalDateTime.now(), +) { + + var reservation: MailReservation? = null + + var modifiedDateTime: LocalDateTime = modifiedDateTime + private set + + companion object { + fun of( + subject: String, + body: String, + sender: String, + recipients: List, + creatorId: Long + ): MailMessage { + return MailMessage( + subject = subject, + body = body, + sender = sender, + recipients = recipients, + creatorId = creatorId + ) + } + + fun withReservation( + subject: String, + body: String, + sender: String, + recipients: List, + reservationTime: LocalDateTime, + creatorId: Long + ): MailMessage { + return MailMessage( + subject = subject, + body = body, + sender = sender, + recipients = recipients, + creatorId = creatorId + ).apply { + this.reservation = MailReservation() + } + } + } +} diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt new file mode 100644 index 000000000..5f10b7f2b --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -0,0 +1,3 @@ +package apply.domain.mail + +class MailReservation diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index fa828d416..f2740a542 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -2,6 +2,7 @@ package apply import apply.application.mail.MailData import apply.domain.mail.MailHistory +import apply.domain.mail.MailMessage import java.time.LocalDateTime private const val SUBJECT: String = "메일제목" @@ -9,6 +10,7 @@ private const val BODY: String = "메일 본문 입니다." private const val SENDER: String = "woowacourse@email.com" private val RECIPIENTS: List = listOf("test1@email.com", "test2@email.com") private val SENT_TIME: LocalDateTime = LocalDateTime.now() +private val RESERVATION_TIME: LocalDateTime = LocalDateTime.now().plusHours(3) fun createMailHistory( subject: String = SUBJECT, @@ -31,3 +33,26 @@ fun createMailData( ): MailData { return MailData(subject, body, sender, recipients, sentTime, id = id) } + +fun createMailMessage( + subject: String = SUBJECT, + body: String = BODY, + sender: String = SENDER, + recipients: List = RECIPIENTS, + id: Long = 0L, + createId: Long = 0L +): MailMessage { + return MailMessage.of(subject, body, sender, recipients, createId) +} + +fun createReservationMailMessage( + subject: String = SUBJECT, + body: String = BODY, + sender: String = SENDER, + recipients: List = RECIPIENTS, + reservationTime: LocalDateTime = RESERVATION_TIME, + id: Long = 0L, + createId: Long = 0L +): MailMessage { + return MailMessage.withReservation(subject, body, sender, recipients, reservationTime, createId) +} diff --git a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt new file mode 100644 index 000000000..184112520 --- /dev/null +++ b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt @@ -0,0 +1,25 @@ +package apply.domain.mail + +import apply.createMailMessage +import apply.createReservationMailMessage +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe + +class MailMessageTest : StringSpec({ + "즉시 발송하는 메일 메시지를 생성한다" { + val mailMessage = createMailMessage(subject = "제목", body = "내용") + + mailMessage.subject shouldBe "제목" + mailMessage.body shouldBe "내용" + mailMessage.reservation shouldBe null + } + + "예약 메일 메시지를 생성한다" { + val mailMessage = createReservationMailMessage(subject = "제목", body = "내용") + + mailMessage.subject shouldBe "제목" + mailMessage.body shouldBe "내용" + mailMessage.reservation shouldNotBe null + } +}) From 559f00a4448e81a24b7cb8690a0701262f1b6414 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 27 Sep 2023 08:42:14 +0900 Subject: [PATCH 02/40] feat: add MailReservation --- .../kotlin/apply/domain/mail/MailMessage.kt | 6 +- .../apply/domain/mail/MailReservation.kt | 35 +++++++++++- .../kotlin/apply/MailHistoryFixtures.kt.kt | 19 ++++++- .../apply/domain/mail/MailReservationTest.kt | 56 +++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/apply/domain/mail/MailReservationTest.kt diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index 95d149052..364f1a955 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -49,7 +49,11 @@ class MailMessage private constructor( recipients = recipients, creatorId = creatorId ).apply { - this.reservation = MailReservation() + this.reservation = MailReservation( + this, + creatorId = creatorId, + reservationTime = reservationTime + ) } } } diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index 5f10b7f2b..f855ac350 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -1,3 +1,36 @@ package apply.domain.mail -class MailReservation +import java.time.LocalDateTime + +private const val PERIOD_MINUTES: Long = 10L + +class MailReservation( + val mailMessage: MailMessage, + var status: MailReservationStatus = MailReservationStatus.WAITING, + val creatorId: Long, + val reservationTime: LocalDateTime +) { + + init { + require(reservationTime.isAfter(LocalDateTime.now())) { + "예약 메일의 예약시간은 현재시간보다 늦은 시간이어야 합니다." + } + + // TODO: validator 추출 + require(reservationTime.minute % PERIOD_MINUTES == 0L) { + "예약 메일의 예약시간은 10분 단위로 설정해야 합니다." + } + } + + fun process() { + status = MailReservationStatus.SENDING + } + + fun complete() { + status = MailReservationStatus.FINISHED + } +} + +enum class MailReservationStatus { + WAITING, SENDING, FINISHED +} diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index f2740a542..cfb5d277d 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -3,6 +3,7 @@ package apply import apply.application.mail.MailData import apply.domain.mail.MailHistory import apply.domain.mail.MailMessage +import apply.domain.mail.MailReservation import java.time.LocalDateTime private const val SUBJECT: String = "메일제목" @@ -10,7 +11,7 @@ private const val BODY: String = "메일 본문 입니다." private const val SENDER: String = "woowacourse@email.com" private val RECIPIENTS: List = listOf("test1@email.com", "test2@email.com") private val SENT_TIME: LocalDateTime = LocalDateTime.now() -private val RESERVATION_TIME: LocalDateTime = LocalDateTime.now().plusHours(3) +private val RESERVATION_TIME: LocalDateTime = LocalDateTime.now().plusHours(3).withMinute(0) fun createMailHistory( subject: String = SUBJECT, @@ -56,3 +57,19 @@ fun createReservationMailMessage( ): MailMessage { return MailMessage.withReservation(subject, body, sender, recipients, reservationTime, createId) } + +fun createMailReservation( + subject: String = SUBJECT, + body: String = BODY, + sender: String = SENDER, + recipients: List = RECIPIENTS, + reservationTime: LocalDateTime = RESERVATION_TIME, + id: Long = 0L, + createId: Long = 0L +): MailReservation { + return MailReservation( + createMailMessage(subject, body, sender, recipients, id, createId), + reservationTime = reservationTime, + creatorId = createId + ) +} diff --git a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt new file mode 100644 index 000000000..36638f412 --- /dev/null +++ b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt @@ -0,0 +1,56 @@ +package apply.domain.mail + +import apply.createMailReservation +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import java.time.LocalDateTime.now + +class MailReservationTest : StringSpec({ + "메일 예약을 생성한다" { + val mailReservation = createMailReservation() + + mailReservation shouldNotBe null + mailReservation.status shouldBe MailReservationStatus.WAITING + } + + "예약 메일의 예약 시간은 10분 단위로 설정할 수 있다" { + val future = now().plusHours(1) + val mailReservation = createMailReservation(reservationTime = future.withMinute(30)) + + mailReservation shouldNotBe null + } + + "예약 메일의 예약 시간이 10분 단위가 아닐 경우 예약이 불가능하다" { + val future = now().plusHours(1) + + listOf(future.withMinute(7), future.withMinute(23)).forAll { reservationTime -> + shouldThrow { + createMailReservation(reservationTime = reservationTime) + } + } + } + + "예약 메일의 예약 시간이 과거라면 예약이 불가능하다" { + val past = now().minusHours(2) + shouldThrow { + createMailReservation(reservationTime = past.withMinute(0)) + } + } + + "예약 메일 전송을 시작한다" { + val mailReservation = createMailReservation() + + mailReservation.process() + mailReservation.status shouldBe MailReservationStatus.SENDING + } + + "예약 메일 전송을 완료한다" { + val mailReservation = createMailReservation() + + mailReservation.complete() + mailReservation.status shouldBe MailReservationStatus.FINISHED + } +}) From faa24f54a8c602539c45873e7a06c8f8e0a72b1e Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 08:25:28 +0900 Subject: [PATCH 03/40] feat: add MailHistory --- .../kotlin/apply/domain/mail/MailHistory2.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/kotlin/apply/domain/mail/MailHistory2.kt diff --git a/src/main/kotlin/apply/domain/mail/MailHistory2.kt b/src/main/kotlin/apply/domain/mail/MailHistory2.kt new file mode 100644 index 000000000..fc7bc07ab --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailHistory2.kt @@ -0,0 +1,20 @@ +package apply.domain.mail + +import java.time.LocalDateTime + +class MailHistory2 private constructor( + val mailMessage: MailMessage, + val recipients: List, + val success: Boolean, + val sentTime: LocalDateTime = LocalDateTime.now() +) { + companion object { + fun ofSuccess(mailMessage: MailMessage, recipients: List): MailHistory2 { + return MailHistory2(mailMessage, recipients, true) + } + + fun ofFailure(mailMessage: MailMessage, recipients: List): MailHistory2 { + return MailHistory2(mailMessage, recipients, false) + } + } +} From 81f618865b2dc531ee7a7ddffd0adc39c425dfe9 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 08:25:43 +0900 Subject: [PATCH 04/40] feat: mapping relationships with mail message and reservation --- .../kotlin/apply/domain/mail/MailMessage.kt | 45 +++++++++++-- .../domain/mail/MailMessageRepository.kt | 5 ++ .../apply/domain/mail/MailReservation.kt | 27 +++++++- .../domain/mail/MailReservationRepository.kt | 5 ++ ...V3_1__Add_mail_message_and_reservation.sql | 29 ++++++++ .../domain/mail/MailMessageRepositoryTest.kt | 67 +++++++++++++++++++ .../apply/domain/mail/MailMessageTest.kt | 4 +- 7 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/apply/domain/mail/MailMessageRepository.kt create mode 100644 src/main/kotlin/apply/domain/mail/MailReservationRepository.kt create mode 100644 src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql create mode 100644 src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index 364f1a955..1636b5316 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -1,22 +1,57 @@ package apply.domain.mail +import support.domain.BaseEntity +import support.domain.StringToListConverter import java.time.LocalDateTime +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Convert +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.Lob +import javax.persistence.OneToMany +@Entity class MailMessage private constructor( + @Column(nullable = false) val subject: String, + + @Column(nullable = false) + @Lob val body: String, + + @Column(nullable = false) val sender: String, + + @Column(nullable = false) + @Convert(converter = StringToListConverter::class) + @Lob val recipients: List, + + @Column(nullable = false) val creatorId: Long, + + @Column(nullable = false) val createdDateTime: LocalDateTime = LocalDateTime.now(), modifiedDateTime: LocalDateTime = LocalDateTime.now(), -) { + id: Long = 0L +) : BaseEntity(id) { - var reservation: MailReservation? = null + @OneToMany(mappedBy = "mailMessage", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) + var reservations: MutableList = mutableListOf() + @Column(nullable = false) var modifiedDateTime: LocalDateTime = modifiedDateTime private set + fun reservation(): MailReservation? { + if (reservations.isEmpty()) { + return null + } + + return reservations.first() + } + companion object { fun of( subject: String, @@ -49,10 +84,8 @@ class MailMessage private constructor( recipients = recipients, creatorId = creatorId ).apply { - this.reservation = MailReservation( - this, - creatorId = creatorId, - reservationTime = reservationTime + reservations.add( + MailReservation(this, creatorId = creatorId, reservationTime = reservationTime) ) } } diff --git a/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt b/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt new file mode 100644 index 000000000..5e114b0c7 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt @@ -0,0 +1,5 @@ +package apply.domain.mail + +import org.springframework.data.jpa.repository.JpaRepository + +interface MailMessageRepository : JpaRepository diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index f855ac350..df88ca1f1 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -1,15 +1,38 @@ package apply.domain.mail +import support.domain.BaseEntity import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.ForeignKey +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne private const val PERIOD_MINUTES: Long = 10L +@Entity class MailReservation( + + @ManyToOne + @JoinColumn( + name = "mail_message_id", nullable = false, + foreignKey = ForeignKey(name = "fk_mail_reservation_to_mail_message") + ) val mailMessage: MailMessage, + + @Column(nullable = false) + @Enumerated(EnumType.STRING) var status: MailReservationStatus = MailReservationStatus.WAITING, + + @Column(nullable = false) val creatorId: Long, - val reservationTime: LocalDateTime -) { + + @Column(nullable = false) + val reservationTime: LocalDateTime, + id: Long = 0L +) : BaseEntity(id) { init { require(reservationTime.isAfter(LocalDateTime.now())) { diff --git a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt new file mode 100644 index 000000000..64e686438 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt @@ -0,0 +1,5 @@ +package apply.domain.mail + +import org.springframework.data.jpa.repository.JpaRepository + +interface MailReservationRepository : JpaRepository diff --git a/src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql b/src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql new file mode 100644 index 000000000..70f91f7b8 --- /dev/null +++ b/src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql @@ -0,0 +1,29 @@ +create table mail_message +( + id bigint not null auto_increment, + subject varchar(255) not null, + body longtext not null, + sender varchar(255) not null, + recipients longtext not null, + creator_id bigint not null, + created_date_time datetime(6) not null, + modified_date_time datetime(6) not null, + primary key (id) +) engine = InnoDB + default charset = utf8mb4; + +create table mail_reservation +( + id bigint not null auto_increment, + status varchar(8) not null, + mail_message_id bigint not null, + creator_id bigint not null, + reservation_time datetime(6) not null, + primary key (id) +) engine = InnoDB + default charset = utf8mb4; + +alter table mail_reservation + add constraint fk_mail_reservation_to_mail_message + foreign key (mail_message_id) + references mail_message (id); diff --git a/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt new file mode 100644 index 000000000..0b9e1ee3b --- /dev/null +++ b/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt @@ -0,0 +1,67 @@ +package apply.domain.mail + +import apply.createMailMessage +import apply.createReservationMailMessage +import io.kotest.core.spec.style.ExpectSpec +import io.kotest.extensions.spring.SpringExtension +import io.kotest.matchers.longs.shouldNotBeZero +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager +import support.test.RepositoryTest +import support.test.spec.afterRootTest +import java.time.LocalDateTime.now + +@RepositoryTest +class MailMessageRepositoryTest( + private val mailMessageRepository: MailMessageRepository, + private val entityManager: TestEntityManager +) : ExpectSpec({ + extensions(SpringExtension) + + context("메일 메시지 저장") { + expect("즉시 발송할 메일 메시지를 저장한다") { + val actual = mailMessageRepository.save(createMailMessage()) + actual.id.shouldNotBeZero() + } + + expect("메일 메시지와 함께 메일 예약을 저장한다") { + val actual = mailMessageRepository.save(createReservationMailMessage()) + actual.id.shouldNotBeZero() + actual.reservation().shouldNotBeNull() + actual.reservation()!!.id.shouldNotBeZero() + } + } + + context("즉시 발송 메일 메시지 조회") { + val mailMessage = mailMessageRepository.save(createMailMessage()) + + expect("메일 메시지만 조회한다") { + val actual = mailMessageRepository.findById(mailMessage.id).get() + actual.shouldNotBeNull() + actual.reservation().shouldBeNull() + } + } + + context("예약 메일 메시지 조회") { + val reservationTime = now().plusHours(3).withMinute(10) + val mailMessage = mailMessageRepository.save(createReservationMailMessage(reservationTime = reservationTime)) + + expect("메일 메시지와 함께 메일 예약을 조회한다") { + val actual = mailMessageRepository.findById(mailMessage.id).get() + actual.shouldNotBeNull() + actual.reservation().shouldNotBeNull() + actual.reservation()!!.reservationTime shouldBe reservationTime + } + } + + afterEach { + entityManager.flush() + entityManager.clear() + } + + afterRootTest { + mailMessageRepository.deleteAll() + } +}) diff --git a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt index 184112520..24c7d6b1b 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt @@ -12,7 +12,7 @@ class MailMessageTest : StringSpec({ mailMessage.subject shouldBe "제목" mailMessage.body shouldBe "내용" - mailMessage.reservation shouldBe null + mailMessage.reservation() shouldBe null } "예약 메일 메시지를 생성한다" { @@ -20,6 +20,6 @@ class MailMessageTest : StringSpec({ mailMessage.subject shouldBe "제목" mailMessage.body shouldBe "내용" - mailMessage.reservation shouldNotBe null + mailMessage.reservation() shouldNotBe null } }) From ad730d6472dbd331dc5f9a0091d792f9ffa608cd Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 4 Oct 2023 11:20:32 +0900 Subject: [PATCH 05/40] feat: mapping relationships with mail message and history --- .../kotlin/apply/domain/mail/MailHistory2.kt | 25 +++++++++- .../domain/mail/MailHistory2Repository.kt | 5 ++ .../db/migration/V3_2__Add_mail_history.sql | 15 ++++++ .../domain/mail/MailHistory2RepositoryTest.kt | 48 +++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt create mode 100644 src/main/resources/db/migration/V3_2__Add_mail_history.sql create mode 100644 src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt diff --git a/src/main/kotlin/apply/domain/mail/MailHistory2.kt b/src/main/kotlin/apply/domain/mail/MailHistory2.kt index fc7bc07ab..a5b89a656 100644 --- a/src/main/kotlin/apply/domain/mail/MailHistory2.kt +++ b/src/main/kotlin/apply/domain/mail/MailHistory2.kt @@ -1,13 +1,34 @@ package apply.domain.mail +import support.domain.BaseEntity +import support.domain.StringToListConverter import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.Convert +import javax.persistence.Entity +import javax.persistence.ForeignKey +import javax.persistence.JoinColumn +import javax.persistence.Lob +import javax.persistence.ManyToOne +@Entity class MailHistory2 private constructor( + @ManyToOne + @JoinColumn(nullable = false, foreignKey = ForeignKey(name = "fk_mail_history_to_mail_message")) val mailMessage: MailMessage, + + @Column(nullable = false) + @Convert(converter = StringToListConverter::class) + @Lob val recipients: List, + + @Column(nullable = false) val success: Boolean, - val sentTime: LocalDateTime = LocalDateTime.now() -) { + + @Column(nullable = false) + val sentTime: LocalDateTime = LocalDateTime.now(), + id: Long = 0L +) : BaseEntity(id) { companion object { fun ofSuccess(mailMessage: MailMessage, recipients: List): MailHistory2 { return MailHistory2(mailMessage, recipients, true) diff --git a/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt b/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt new file mode 100644 index 000000000..9b88ff7d8 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt @@ -0,0 +1,5 @@ +package apply.domain.mail + +import org.springframework.data.jpa.repository.JpaRepository + +interface MailHistory2Repository : JpaRepository diff --git a/src/main/resources/db/migration/V3_2__Add_mail_history.sql b/src/main/resources/db/migration/V3_2__Add_mail_history.sql new file mode 100644 index 000000000..c61ddf21d --- /dev/null +++ b/src/main/resources/db/migration/V3_2__Add_mail_history.sql @@ -0,0 +1,15 @@ +create table mail_history2 +( + id bigint not null auto_increment, + recipients longtext not null, + sent_time datetime(6) not null, + success bit(1) not null, + mail_message_id bigint not null, + primary key (id) +) engine = InnoDB + default charset = utf8mb4; + +alter table mail_history2 + add constraint fk_mail_history_to_mail_message + foreign key (mail_message_id) + references mail_message (id); \ No newline at end of file diff --git a/src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt new file mode 100644 index 000000000..069690867 --- /dev/null +++ b/src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt @@ -0,0 +1,48 @@ +package apply.domain.mail + +import apply.createMailMessage +import io.kotest.core.spec.style.ExpectSpec +import io.kotest.extensions.spring.SpringExtension +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.longs.shouldNotBeZero +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager +import support.test.RepositoryTest +import support.test.spec.afterRootTest + +@RepositoryTest +class MailHistory2RepositoryTest( + private val mailMessageRepository: MailMessageRepository, + private val mailHistory2Repository: MailHistory2Repository, + private val entityManager: TestEntityManager +) : ExpectSpec({ + extensions(SpringExtension) + + context("메일 히스토리 저장") { + val mailMessage = mailMessageRepository.save(createMailMessage()) + + expect("메일 발송 성공에 대한 히스토리를 저장한다") { + val actual = mailHistory2Repository.save(MailHistory2.ofSuccess(mailMessage, mailMessage.recipients)) + actual.id.shouldNotBeZero() + } + } + + context("메일 히스토리 조회") { + val mailMessage = mailMessageRepository.save(createMailMessage()) + mailHistory2Repository.save(MailHistory2.ofSuccess(mailMessage, mailMessage.recipients)) + + expect("메일 발송 성공에 대한 히스토리를 저장한다") { + val actual = mailHistory2Repository.findAll() + actual.shouldNotBeEmpty() + actual[0].mailMessage.id.shouldNotBeZero() + } + } + + afterEach { + entityManager.flush() + entityManager.clear() + } + + afterRootTest { + mailHistory2Repository.deleteAll() + } +}) From 742c8a621544df0c6dfec8abf9919c29c293a00a Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 4 Oct 2023 13:27:54 +0900 Subject: [PATCH 06/40] feat: using new MailHistory --- .../apply/application/MailHistoryService.kt | 8 +++-- .../kotlin/apply/application/mail/MailData.kt | 16 ++++++++++ .../apply/application/mail/MailService.kt | 28 ++++++++++------- .../apply/config/DatabaseInitializer.kt | 31 ++++++++++++++----- .../domain/mail/MailHistory2Repository.kt | 4 +++ .../kotlin/apply/MailHistoryFixtures.kt.kt | 10 ++++++ .../application/MailHistoryServiceTest.kt | 20 ++++++------ 7 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/apply/application/MailHistoryService.kt b/src/main/kotlin/apply/application/MailHistoryService.kt index a9ff4b9bb..47e17d29e 100644 --- a/src/main/kotlin/apply/application/MailHistoryService.kt +++ b/src/main/kotlin/apply/application/MailHistoryService.kt @@ -1,6 +1,7 @@ package apply.application import apply.application.mail.MailData +import apply.domain.mail.MailHistory2Repository import apply.domain.mail.MailHistoryRepository import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service @@ -9,14 +10,15 @@ import org.springframework.transaction.annotation.Transactional @Transactional @Service class MailHistoryService( - private val mailHistoryRepository: MailHistoryRepository + private val mailHistoryRepository: MailHistoryRepository, + private val mailHistory2Repository: MailHistory2Repository ) { fun findAll(): List { - return mailHistoryRepository.findAll().map { MailData(it) } + return mailHistory2Repository.findAll().map { MailData(it) } } fun getById(mailHistoryId: Long): MailData { - val mailHistory = mailHistoryRepository.getOrThrow(mailHistoryId) + val mailHistory = mailHistory2Repository.getOrThrow(mailHistoryId) return MailData(mailHistory) } } diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index 392f38225..47268b6a1 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -1,6 +1,8 @@ package apply.application.mail import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistory2 +import apply.domain.mail.MailMessage import org.springframework.core.io.ByteArrayResource import java.time.LocalDateTime import javax.validation.constraints.NotEmpty @@ -38,4 +40,18 @@ data class MailData( mailHistory.sentTime, id = mailHistory.id ) + + constructor(mailHistory: MailHistory2) : this( + mailHistory.mailMessage.subject, + mailHistory.mailMessage.body, + mailHistory.mailMessage.sender, + mailHistory.recipients, + mailHistory.sentTime, + id = mailHistory.id + ) + + fun toMailMessage(): MailMessage { + // TODO: 작성자 ID 바인딩 + return MailMessage.of(subject, body, sender, recipients, 1L) + } } diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/MailService.kt index 9819ec7cc..82232a47d 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/MailService.kt @@ -2,8 +2,10 @@ package apply.application.mail import apply.application.ApplicationProperties import apply.domain.applicationform.ApplicationFormSubmittedEvent -import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistory2 +import apply.domain.mail.MailHistory2Repository import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.MailMessageRepository import apply.domain.recruitment.RecruitmentRepository import apply.domain.recruitment.getOrThrow import apply.domain.user.PasswordResetEvent @@ -25,6 +27,8 @@ class MailService( private val userRepository: UserRepository, private val recruitmentRepository: RecruitmentRepository, private val mailHistoryRepository: MailHistoryRepository, + private val mailMessageRepository: MailMessageRepository, + private val mailHistory2Repository: MailHistory2Repository, private val applicationProperties: ApplicationProperties, private val templateEngine: ISpringTemplateEngine, private val mailSender: MailSender, @@ -90,10 +94,10 @@ class MailService( @Async fun sendMailsByBcc(request: MailData, files: Map) { + val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username - // TODO: 성공과 실패를 분리하여 히스토리 관리 val succeeded = mutableListOf() val failed = mutableListOf() for (addresses in recipients.chunked(MAIL_SENDING_UNIT)) { @@ -102,15 +106,17 @@ class MailService( .onFailure { failed.addAll(addresses) } } - mailHistoryRepository.save( - MailHistory( - request.subject, - request.body, - request.sender, - request.recipients, - request.sentTime - ) - ) + val mailHistories = mutableListOf() + + if (succeeded.isNotEmpty()) { + mailHistories.add(MailHistory2.ofSuccess(mailMessage, succeeded)) + } + + if (failed.isNotEmpty()) { + mailHistories.add(MailHistory2.ofFailure(mailMessage, failed)) + } + + mailHistory2Repository.saveAll(mailHistories) } fun generateMailBody(mailData: MailData): String { diff --git a/src/main/kotlin/apply/config/DatabaseInitializer.kt b/src/main/kotlin/apply/config/DatabaseInitializer.kt index 3d6e5394f..ffdd69bb9 100644 --- a/src/main/kotlin/apply/config/DatabaseInitializer.kt +++ b/src/main/kotlin/apply/config/DatabaseInitializer.kt @@ -20,8 +20,11 @@ import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.judgmentitem.JudgmentItem import apply.domain.judgmentitem.JudgmentItemRepository import apply.domain.judgmentitem.ProgrammingLanguage -import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistory2 +import apply.domain.mail.MailHistory2Repository import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.MailMessage +import apply.domain.mail.MailMessageRepository import apply.domain.mission.Mission import apply.domain.mission.MissionRepository import apply.domain.recruitment.Recruitment @@ -58,6 +61,8 @@ class DatabaseInitializer( private val judgmentItemRepository: JudgmentItemRepository, private val assignmentRepository: AssignmentRepository, private val mailHistoryRepository: MailHistoryRepository, + private val mailMessageRepository: MailMessageRepository, + private val mailHistory2Repository: MailHistory2Repository, private val database: Database ) : CommandLineRunner { override fun run(vararg args: String) { @@ -429,15 +434,25 @@ class DatabaseInitializer( } private fun populateMailHistories() { + val mailMessage = MailMessage.of( + subject = "[우아한테크코스] 프리코스를 진행하는 목적과 사전 준비", + body = "안녕하세요.", + sender = "woowa_course@woowahan.com", + recipients = listOf("a@email.com", "b@email.com", "c@email.com", "d@email.com"), + creatorId = 1L, + ) + mailMessageRepository.save(mailMessage) + val mailHistories = listOf( - MailHistory( - subject = "[우아한테크코스] 프리코스를 진행하는 목적과 사전 준비", - body = "안녕하세요.", - sender = "woowa_course@woowahan.com", - recipients = listOf("a@email.com", "b@email.com", "c@email.com", "d@email.com"), - sentTime = createLocalDateTime(2020, 11, 5, 10) + MailHistory2.ofSuccess( + mailMessage = mailMessage, + recipients = mailMessage.recipients.subList(0, 2) + ), + MailHistory2.ofSuccess( + mailMessage = mailMessage, + recipients = mailMessage.recipients.subList(3, 4) ) ) - mailHistoryRepository.saveAll(mailHistories) + mailHistory2Repository.saveAll(mailHistories) } } diff --git a/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt b/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt index 9b88ff7d8..7ab8009d9 100644 --- a/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt +++ b/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt @@ -1,5 +1,9 @@ package apply.domain.mail import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun MailHistory2Repository.getOrThrow(id: Long): MailHistory2 = findByIdOrNull(id) + ?: throw NoSuchElementException("메일 이력이 존재하지 않습니다. id: $id") interface MailHistory2Repository : JpaRepository diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index cfb5d277d..b35945bb8 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -2,6 +2,7 @@ package apply import apply.application.mail.MailData import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistory2 import apply.domain.mail.MailMessage import apply.domain.mail.MailReservation import java.time.LocalDateTime @@ -73,3 +74,12 @@ fun createMailReservation( creatorId = createId ) } + +fun createSuccessMailHistory2( + subject: String = SUBJECT, + body: String = BODY, + sender: String = SENDER, + recipients: List = RECIPIENTS +): MailHistory2 { + return MailHistory2.ofSuccess(createMailMessage(subject, body, sender, recipients), recipients) +} diff --git a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt index ad0ab9fdb..71dea86d9 100644 --- a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt +++ b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt @@ -1,7 +1,7 @@ package apply.application -import apply.createMailData -import apply.createMailHistory +import apply.createSuccessMailHistory2 +import apply.domain.mail.MailHistory2Repository import apply.domain.mail.MailHistoryRepository import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe @@ -9,27 +9,25 @@ import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import support.test.spec.afterRootTest -import java.time.LocalDateTime.now class MailHistoryServiceTest : BehaviorSpec({ val mailHistoryRepository = mockk() + val mailHistory2Repository = mockk() - val mailHistoryService = MailHistoryService(mailHistoryRepository) + val mailHistoryService = MailHistoryService(mailHistoryRepository, mailHistory2Repository) Given("메일 이력이 있는 경우") { - val now = now() - - every { mailHistoryRepository.findAll() } returns listOf( - createMailHistory(subject = "제목1", sentTime = now), - createMailHistory(subject = "제목2", sentTime = now.plusSeconds(1)) + every { mailHistory2Repository.findAll() } returns listOf( + createSuccessMailHistory2(subject = "제목1"), + createSuccessMailHistory2(subject = "제목2") ) When("모든 메일 이력을 조회하면") { val actual = mailHistoryService.findAll() Then("모든 메일 이력을 확인할 수 있다") { - actual[0] shouldBe createMailData(subject = "제목1", sentTime = now) - actual[1] shouldBe createMailData(subject = "제목2", sentTime = now.plusSeconds(1)) + actual[0].subject shouldBe "제목1" + actual[1].subject shouldBe "제목2" } } } From 1d6032b42ad2e372e84c896de8691288a374d7d4 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 4 Oct 2023 13:41:32 +0900 Subject: [PATCH 07/40] refactor: delete old MailHistory --- .../apply/application/MailHistoryService.kt | 8 ++-- .../kotlin/apply/application/mail/MailData.kt | 10 ----- .../apply/application/mail/MailService.kt | 14 +++---- .../apply/config/DatabaseInitializer.kt | 12 +++--- .../kotlin/apply/domain/mail/MailHistory.kt | 32 ++++++++++----- .../kotlin/apply/domain/mail/MailHistory2.kt | 41 ------------------- .../domain/mail/MailHistory2Repository.kt | 9 ---- .../migration/V3_3__rename_mail_history.sql | 3 ++ .../kotlin/apply/MailHistoryFixtures.kt.kt | 16 +------- .../application/MailHistoryServiceTest.kt | 6 +-- ...ryTest.kt => MailHistoryRepositoryTest.kt} | 12 +++--- 11 files changed, 48 insertions(+), 115 deletions(-) delete mode 100644 src/main/kotlin/apply/domain/mail/MailHistory2.kt delete mode 100644 src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt create mode 100644 src/main/resources/db/migration/V3_3__rename_mail_history.sql rename src/test/kotlin/apply/domain/mail/{MailHistory2RepositoryTest.kt => MailHistoryRepositoryTest.kt} (75%) diff --git a/src/main/kotlin/apply/application/MailHistoryService.kt b/src/main/kotlin/apply/application/MailHistoryService.kt index 47e17d29e..a9ff4b9bb 100644 --- a/src/main/kotlin/apply/application/MailHistoryService.kt +++ b/src/main/kotlin/apply/application/MailHistoryService.kt @@ -1,7 +1,6 @@ package apply.application import apply.application.mail.MailData -import apply.domain.mail.MailHistory2Repository import apply.domain.mail.MailHistoryRepository import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service @@ -10,15 +9,14 @@ import org.springframework.transaction.annotation.Transactional @Transactional @Service class MailHistoryService( - private val mailHistoryRepository: MailHistoryRepository, - private val mailHistory2Repository: MailHistory2Repository + private val mailHistoryRepository: MailHistoryRepository ) { fun findAll(): List { - return mailHistory2Repository.findAll().map { MailData(it) } + return mailHistoryRepository.findAll().map { MailData(it) } } fun getById(mailHistoryId: Long): MailData { - val mailHistory = mailHistory2Repository.getOrThrow(mailHistoryId) + val mailHistory = mailHistoryRepository.getOrThrow(mailHistoryId) return MailData(mailHistory) } } diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index 47268b6a1..d509f391f 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -1,7 +1,6 @@ package apply.application.mail import apply.domain.mail.MailHistory -import apply.domain.mail.MailHistory2 import apply.domain.mail.MailMessage import org.springframework.core.io.ByteArrayResource import java.time.LocalDateTime @@ -33,15 +32,6 @@ data class MailData( var id: Long = 0L ) { constructor(mailHistory: MailHistory) : this( - mailHistory.subject, - mailHistory.body, - mailHistory.sender, - mailHistory.recipients, - mailHistory.sentTime, - id = mailHistory.id - ) - - constructor(mailHistory: MailHistory2) : this( mailHistory.mailMessage.subject, mailHistory.mailMessage.body, mailHistory.mailMessage.sender, diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/MailService.kt index 82232a47d..a8f25d3ab 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/MailService.kt @@ -2,8 +2,7 @@ package apply.application.mail import apply.application.ApplicationProperties import apply.domain.applicationform.ApplicationFormSubmittedEvent -import apply.domain.mail.MailHistory2 -import apply.domain.mail.MailHistory2Repository +import apply.domain.mail.MailHistory import apply.domain.mail.MailHistoryRepository import apply.domain.mail.MailMessageRepository import apply.domain.recruitment.RecruitmentRepository @@ -26,9 +25,8 @@ private const val MAIL_SENDING_UNIT: Int = 50 class MailService( private val userRepository: UserRepository, private val recruitmentRepository: RecruitmentRepository, - private val mailHistoryRepository: MailHistoryRepository, private val mailMessageRepository: MailMessageRepository, - private val mailHistory2Repository: MailHistory2Repository, + private val mailHistoryRepository: MailHistoryRepository, private val applicationProperties: ApplicationProperties, private val templateEngine: ISpringTemplateEngine, private val mailSender: MailSender, @@ -106,17 +104,17 @@ class MailService( .onFailure { failed.addAll(addresses) } } - val mailHistories = mutableListOf() + val mailHistories = mutableListOf() if (succeeded.isNotEmpty()) { - mailHistories.add(MailHistory2.ofSuccess(mailMessage, succeeded)) + mailHistories.add(MailHistory.ofSuccess(mailMessage, succeeded)) } if (failed.isNotEmpty()) { - mailHistories.add(MailHistory2.ofFailure(mailMessage, failed)) + mailHistories.add(MailHistory.ofFailure(mailMessage, failed)) } - mailHistory2Repository.saveAll(mailHistories) + mailHistoryRepository.saveAll(mailHistories) } fun generateMailBody(mailData: MailData): String { diff --git a/src/main/kotlin/apply/config/DatabaseInitializer.kt b/src/main/kotlin/apply/config/DatabaseInitializer.kt index ffdd69bb9..a1432e446 100644 --- a/src/main/kotlin/apply/config/DatabaseInitializer.kt +++ b/src/main/kotlin/apply/config/DatabaseInitializer.kt @@ -20,8 +20,7 @@ import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.judgmentitem.JudgmentItem import apply.domain.judgmentitem.JudgmentItemRepository import apply.domain.judgmentitem.ProgrammingLanguage -import apply.domain.mail.MailHistory2 -import apply.domain.mail.MailHistory2Repository +import apply.domain.mail.MailHistory import apply.domain.mail.MailHistoryRepository import apply.domain.mail.MailMessage import apply.domain.mail.MailMessageRepository @@ -60,9 +59,8 @@ class DatabaseInitializer( private val missionRepository: MissionRepository, private val judgmentItemRepository: JudgmentItemRepository, private val assignmentRepository: AssignmentRepository, - private val mailHistoryRepository: MailHistoryRepository, private val mailMessageRepository: MailMessageRepository, - private val mailHistory2Repository: MailHistory2Repository, + private val mailHistoryRepository: MailHistoryRepository, private val database: Database ) : CommandLineRunner { override fun run(vararg args: String) { @@ -444,15 +442,15 @@ class DatabaseInitializer( mailMessageRepository.save(mailMessage) val mailHistories = listOf( - MailHistory2.ofSuccess( + MailHistory.ofSuccess( mailMessage = mailMessage, recipients = mailMessage.recipients.subList(0, 2) ), - MailHistory2.ofSuccess( + MailHistory.ofSuccess( mailMessage = mailMessage, recipients = mailMessage.recipients.subList(3, 4) ) ) - mailHistory2Repository.saveAll(mailHistories) + mailHistoryRepository.saveAll(mailHistories) } } diff --git a/src/main/kotlin/apply/domain/mail/MailHistory.kt b/src/main/kotlin/apply/domain/mail/MailHistory.kt index 40f9e8c12..7178006ff 100644 --- a/src/main/kotlin/apply/domain/mail/MailHistory.kt +++ b/src/main/kotlin/apply/domain/mail/MailHistory.kt @@ -6,26 +6,36 @@ import java.time.LocalDateTime import javax.persistence.Column import javax.persistence.Convert import javax.persistence.Entity +import javax.persistence.ForeignKey +import javax.persistence.JoinColumn import javax.persistence.Lob +import javax.persistence.ManyToOne @Entity -class MailHistory( - @Column(nullable = false) - val subject: String, - - @Column(nullable = false) - @Lob - val body: String, - - @Column(nullable = false) - val sender: String, +class MailHistory private constructor( + @ManyToOne + @JoinColumn(nullable = false, foreignKey = ForeignKey(name = "fk_mail_history_to_mail_message")) + val mailMessage: MailMessage, @Column(nullable = false) @Convert(converter = StringToListConverter::class) @Lob val recipients: List, + @Column(nullable = false) + val success: Boolean, + @Column(nullable = false) val sentTime: LocalDateTime = LocalDateTime.now(), id: Long = 0L -) : BaseEntity(id) +) : BaseEntity(id) { + companion object { + fun ofSuccess(mailMessage: MailMessage, recipients: List): MailHistory { + return MailHistory(mailMessage, recipients, true) + } + + fun ofFailure(mailMessage: MailMessage, recipients: List): MailHistory { + return MailHistory(mailMessage, recipients, false) + } + } +} diff --git a/src/main/kotlin/apply/domain/mail/MailHistory2.kt b/src/main/kotlin/apply/domain/mail/MailHistory2.kt deleted file mode 100644 index a5b89a656..000000000 --- a/src/main/kotlin/apply/domain/mail/MailHistory2.kt +++ /dev/null @@ -1,41 +0,0 @@ -package apply.domain.mail - -import support.domain.BaseEntity -import support.domain.StringToListConverter -import java.time.LocalDateTime -import javax.persistence.Column -import javax.persistence.Convert -import javax.persistence.Entity -import javax.persistence.ForeignKey -import javax.persistence.JoinColumn -import javax.persistence.Lob -import javax.persistence.ManyToOne - -@Entity -class MailHistory2 private constructor( - @ManyToOne - @JoinColumn(nullable = false, foreignKey = ForeignKey(name = "fk_mail_history_to_mail_message")) - val mailMessage: MailMessage, - - @Column(nullable = false) - @Convert(converter = StringToListConverter::class) - @Lob - val recipients: List, - - @Column(nullable = false) - val success: Boolean, - - @Column(nullable = false) - val sentTime: LocalDateTime = LocalDateTime.now(), - id: Long = 0L -) : BaseEntity(id) { - companion object { - fun ofSuccess(mailMessage: MailMessage, recipients: List): MailHistory2 { - return MailHistory2(mailMessage, recipients, true) - } - - fun ofFailure(mailMessage: MailMessage, recipients: List): MailHistory2 { - return MailHistory2(mailMessage, recipients, false) - } - } -} diff --git a/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt b/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt deleted file mode 100644 index 7ab8009d9..000000000 --- a/src/main/kotlin/apply/domain/mail/MailHistory2Repository.kt +++ /dev/null @@ -1,9 +0,0 @@ -package apply.domain.mail - -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.repository.findByIdOrNull - -fun MailHistory2Repository.getOrThrow(id: Long): MailHistory2 = findByIdOrNull(id) - ?: throw NoSuchElementException("메일 이력이 존재하지 않습니다. id: $id") - -interface MailHistory2Repository : JpaRepository diff --git a/src/main/resources/db/migration/V3_3__rename_mail_history.sql b/src/main/resources/db/migration/V3_3__rename_mail_history.sql new file mode 100644 index 000000000..1afec532d --- /dev/null +++ b/src/main/resources/db/migration/V3_3__rename_mail_history.sql @@ -0,0 +1,3 @@ +rename table mail_history to mail_history_old; + +rename table mail_history2 to mail_history; diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index b35945bb8..64a20e53c 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -2,7 +2,6 @@ package apply import apply.application.mail.MailData import apply.domain.mail.MailHistory -import apply.domain.mail.MailHistory2 import apply.domain.mail.MailMessage import apply.domain.mail.MailReservation import java.time.LocalDateTime @@ -14,17 +13,6 @@ private val RECIPIENTS: List = listOf("test1@email.com", "test2@email.co private val SENT_TIME: LocalDateTime = LocalDateTime.now() private val RESERVATION_TIME: LocalDateTime = LocalDateTime.now().plusHours(3).withMinute(0) -fun createMailHistory( - subject: String = SUBJECT, - body: String = BODY, - sender: String = SENDER, - recipients: List = RECIPIENTS, - sentTime: LocalDateTime = SENT_TIME, - id: Long = 0L -): MailHistory { - return MailHistory(subject, body, sender, recipients, sentTime, id) -} - fun createMailData( subject: String = SUBJECT, body: String = BODY, @@ -80,6 +68,6 @@ fun createSuccessMailHistory2( body: String = BODY, sender: String = SENDER, recipients: List = RECIPIENTS -): MailHistory2 { - return MailHistory2.ofSuccess(createMailMessage(subject, body, sender, recipients), recipients) +): MailHistory { + return MailHistory.ofSuccess(createMailMessage(subject, body, sender, recipients), recipients) } diff --git a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt index 71dea86d9..d6a5b35f4 100644 --- a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt +++ b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt @@ -1,7 +1,6 @@ package apply.application import apply.createSuccessMailHistory2 -import apply.domain.mail.MailHistory2Repository import apply.domain.mail.MailHistoryRepository import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe @@ -12,12 +11,11 @@ import support.test.spec.afterRootTest class MailHistoryServiceTest : BehaviorSpec({ val mailHistoryRepository = mockk() - val mailHistory2Repository = mockk() - val mailHistoryService = MailHistoryService(mailHistoryRepository, mailHistory2Repository) + val mailHistoryService = MailHistoryService(mailHistoryRepository) Given("메일 이력이 있는 경우") { - every { mailHistory2Repository.findAll() } returns listOf( + every { mailHistoryRepository.findAll() } returns listOf( createSuccessMailHistory2(subject = "제목1"), createSuccessMailHistory2(subject = "제목2") ) diff --git a/src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt similarity index 75% rename from src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt rename to src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt index 069690867..a576f8661 100644 --- a/src/test/kotlin/apply/domain/mail/MailHistory2RepositoryTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt @@ -10,9 +10,9 @@ import support.test.RepositoryTest import support.test.spec.afterRootTest @RepositoryTest -class MailHistory2RepositoryTest( +class MailHistoryRepositoryTest( private val mailMessageRepository: MailMessageRepository, - private val mailHistory2Repository: MailHistory2Repository, + private val mailHistoryRepository: MailHistoryRepository, private val entityManager: TestEntityManager ) : ExpectSpec({ extensions(SpringExtension) @@ -21,17 +21,17 @@ class MailHistory2RepositoryTest( val mailMessage = mailMessageRepository.save(createMailMessage()) expect("메일 발송 성공에 대한 히스토리를 저장한다") { - val actual = mailHistory2Repository.save(MailHistory2.ofSuccess(mailMessage, mailMessage.recipients)) + val actual = mailHistoryRepository.save(MailHistory.ofSuccess(mailMessage, mailMessage.recipients)) actual.id.shouldNotBeZero() } } context("메일 히스토리 조회") { val mailMessage = mailMessageRepository.save(createMailMessage()) - mailHistory2Repository.save(MailHistory2.ofSuccess(mailMessage, mailMessage.recipients)) + mailHistoryRepository.save(MailHistory.ofSuccess(mailMessage, mailMessage.recipients)) expect("메일 발송 성공에 대한 히스토리를 저장한다") { - val actual = mailHistory2Repository.findAll() + val actual = mailHistoryRepository.findAll() actual.shouldNotBeEmpty() actual[0].mailMessage.id.shouldNotBeZero() } @@ -43,6 +43,6 @@ class MailHistory2RepositoryTest( } afterRootTest { - mailHistory2Repository.deleteAll() + mailHistoryRepository.deleteAll() } }) From d09c01b8a26dabec0cbaab367d4067b9e5b061c4 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 4 Oct 2023 23:44:55 +0900 Subject: [PATCH 08/40] feat: Add functionality for scheduling email reservations --- .../kotlin/apply/application/mail/MailData.kt | 5 +++ .../kotlin/apply/application/mail/MailDtos.kt | 40 +++++++++++++++++++ .../application/mail/MailMessageService.kt | 16 ++++++++ .../domain/mail/MailMessageRepository.kt | 4 ++ .../mail/MailMessageIntegrationTest.kt | 39 ++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 src/main/kotlin/apply/application/mail/MailDtos.kt create mode 100644 src/main/kotlin/apply/application/mail/MailMessageService.kt create mode 100644 src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index d509f391f..9d325d604 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -44,4 +44,9 @@ data class MailData( // TODO: 작성자 ID 바인딩 return MailMessage.of(subject, body, sender, recipients, 1L) } + + fun toReservationMailMessage(): MailMessage { + // TODO: 작성자 ID 바인딩 + return MailMessage.withReservation(subject, body, sender, recipients, sentTime, 1L) + } } diff --git a/src/main/kotlin/apply/application/mail/MailDtos.kt b/src/main/kotlin/apply/application/mail/MailDtos.kt new file mode 100644 index 000000000..077086850 --- /dev/null +++ b/src/main/kotlin/apply/application/mail/MailDtos.kt @@ -0,0 +1,40 @@ +package apply.application.mail + +import apply.domain.mail.MailMessage +import apply.domain.mail.MailReservation +import apply.domain.mail.MailReservationStatus +import java.time.LocalDateTime + +data class MailMessageResponse( + val subject: String, + val body: String, + val sender: String, + val recipients: List, + val createdDateTime: LocalDateTime, + val modifiedDateTime: LocalDateTime, + val reservation: MailReservationSimpleResponse?, + val id: Long +) { + constructor(mailMessage: MailMessage) : this( + mailMessage.subject, + mailMessage.body, + mailMessage.sender, + mailMessage.recipients, + mailMessage.createdDateTime, + mailMessage.modifiedDateTime, + mailMessage.reservation()?.let { MailReservationSimpleResponse(it) }, + mailMessage.id + ) +} + +data class MailReservationSimpleResponse( + val status: MailReservationStatus, + val reservationTime: LocalDateTime, + val id: Long, +) { + constructor(mailReservation: MailReservation) : this( + mailReservation.status, + mailReservation.reservationTime, + mailReservation.id + ) +} diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt new file mode 100644 index 000000000..1eeaf23c0 --- /dev/null +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -0,0 +1,16 @@ +package apply.application.mail + +import apply.domain.mail.MailMessageRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class MailMessageService( + private val mailMessageRepository: MailMessageRepository +) { + fun reserve(request: MailData): MailMessageResponse { + val mailMessage = mailMessageRepository.save(request.toReservationMailMessage()) + return MailMessageResponse(mailMessage) + } +} diff --git a/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt b/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt index 5e114b0c7..d078e15a8 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessageRepository.kt @@ -1,5 +1,9 @@ package apply.domain.mail import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun MailMessageRepository.getOrThrow(id: Long) = findByIdOrNull(id) + ?: throw NoSuchElementException("메일이 존재하지 않습니다. id: $id") interface MailMessageRepository : JpaRepository diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt new file mode 100644 index 000000000..c7765da33 --- /dev/null +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -0,0 +1,39 @@ +package apply.application.mail + +import apply.createMailData +import apply.domain.mail.MailMessageRepository +import apply.domain.mail.getOrThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.springframework.transaction.annotation.Transactional +import support.test.IntegrationTest +import java.time.LocalDateTime.now + +@Transactional +@IntegrationTest +class MailMessageIntegrationTest( + private val mailMessageService: MailMessageService, + private val mailMessageRepository: MailMessageRepository +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("예약하고 싶은 메일 내용이 있는 경우") { + val reservationTime = now().plusHours(3).withMinute(10) + val mailData = createMailData(sentTime = reservationTime) + + When("메일을 예약하면") { + val mailMessage = mailMessageService.reserve(mailData) + + Then("메일 메시지와 예약이 생성된다") { + val actual = mailMessageRepository.getOrThrow(mailMessage.id) + + actual.subject shouldBe mailData.subject + actual.reservation().shouldNotBeNull() + actual.reservation()!!.reservationTime shouldBe reservationTime + } + } + } +}) From b8168e6a75e05202424a4409feffca4d37eda4a8 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 00:11:38 +0900 Subject: [PATCH 09/40] feat: add functionality for updating mail messages --- .../application/mail/MailMessageService.kt | 7 +++++ .../kotlin/apply/domain/mail/MailMessage.kt | 24 ++++++++++++---- .../apply/domain/mail/MailReservation.kt | 6 ++++ .../mail/MailMessageIntegrationTest.kt | 25 +++++++++++++++++ .../apply/domain/mail/MailMessageTest.kt | 28 +++++++++++++++++++ 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 1eeaf23c0..af2c28202 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -1,6 +1,7 @@ package apply.application.mail import apply.domain.mail.MailMessageRepository +import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -13,4 +14,10 @@ class MailMessageService( val mailMessage = mailMessageRepository.save(request.toReservationMailMessage()) return MailMessageResponse(mailMessage) } + + fun updateReservedMessage(mailMessageId: Long, request: MailData) { + val mailMessage = mailMessageRepository.getOrThrow(mailMessageId) + mailMessage.update(request.subject, request.body, request.recipients) + mailMessageRepository.save(mailMessage) + } } diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index 1636b5316..e96e8c0b8 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -14,19 +14,19 @@ import javax.persistence.OneToMany @Entity class MailMessage private constructor( @Column(nullable = false) - val subject: String, + var subject: String, @Column(nullable = false) @Lob - val body: String, + var body: String, @Column(nullable = false) - val sender: String, + var sender: String, @Column(nullable = false) @Convert(converter = StringToListConverter::class) @Lob - val recipients: List, + var recipients: List, @Column(nullable = false) val creatorId: Long, @@ -45,13 +45,27 @@ class MailMessage private constructor( private set fun reservation(): MailReservation? { - if (reservations.isEmpty()) { + if (!hasReservation()) { return null } return reservations.first() } + fun update(subject: String, body: String, recipients: List) { + check(hasReservation()) + reservation()?.validateStatus() + + this.subject = subject + this.body = body + this.recipients = recipients + this.modifiedDateTime = LocalDateTime.now() + } + + private fun hasReservation(): Boolean { + return reservations.isNotEmpty() + } + companion object { fun of( subject: String, diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index df88ca1f1..a5c5f56bc 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -52,6 +52,12 @@ class MailReservation( fun complete() { status = MailReservationStatus.FINISHED } + + fun validateStatus() { + check(status == MailReservationStatus.WAITING) { + "메일 예약은 WAITING 상태에서만 가능합니다." + } + } } enum class MailReservationStatus { diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index c7765da33..c8f8b300c 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -36,4 +36,29 @@ class MailMessageIntegrationTest( } } } + + Given("메일 예약 후 발송 작업이 아직 시작되지 않았을 경우") { + val reservationTime = now().plusHours(3).withMinute(10) + val mailData = createMailData(sentTime = reservationTime) + val mailMessage = mailMessageService.reserve(mailData) + + When("메일 내용을 변경하면") { + val updatedMailData = createMailData( + subject = "변경된 제목", + body = "변경된 본문", + recipients = listOf("변경된 수신자") + ) + mailMessageService.updateReservedMessage(mailMessage.id, updatedMailData) + + Then("변경된 내용이 반영된다") { + val actual = mailMessageRepository.getOrThrow(mailMessage.id) + + actual.subject shouldBe updatedMailData.subject + actual.body shouldBe updatedMailData.body + actual.recipients shouldBe updatedMailData.recipients + actual.reservation().shouldNotBeNull() + actual.reservation()!!.reservationTime shouldBe reservationTime + } + } + } }) diff --git a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt index 24c7d6b1b..ebe3778a5 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt @@ -2,6 +2,7 @@ package apply.domain.mail import apply.createMailMessage import apply.createReservationMailMessage +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -22,4 +23,31 @@ class MailMessageTest : StringSpec({ mailMessage.body shouldBe "내용" mailMessage.reservation() shouldNotBe null } + + "예약 메일 메시지의 내용을 수정한다" { + val mailMessage = createReservationMailMessage(subject = "제목", body = "내용") + + mailMessage.update("수정된 제목", "수정된 내용", mailMessage.recipients) + + mailMessage.subject shouldBe "수정된 제목" + mailMessage.body shouldBe "수정된 내용" + mailMessage.reservation() shouldNotBe null + } + + "일반 메일 메시지의 내용은 수정할 수 없다" { + val mailMessage = createMailMessage() + + shouldThrow { + mailMessage.update("수정된 제목", "수정된 내용", mailMessage.recipients) + } + } + + "발송 이후 상태인 예약 메일 메시지의 내용은 수정할 수 없다" { + val mailMessage = createReservationMailMessage() + mailMessage.reservation()?.process() + + shouldThrow { + mailMessage.update("수정된 제목", "수정된 내용", mailMessage.recipients) + } + } }) From f12edfae82c136267229f1e44b891e0e11528328 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 00:35:38 +0900 Subject: [PATCH 10/40] feat: add functionality for updating mail's reservation time --- .../kotlin/apply/application/mail/MailDtos.kt | 14 +++++++++ .../mail/MailReservationService.kt | 21 ++++++++++++++ .../apply/domain/mail/MailReservation.kt | 29 +++++++++++++------ .../domain/mail/MailReservationRepository.kt | 4 +++ .../apply/domain/mail/MailReservationTest.kt | 20 +++++++++++++ 5 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/apply/application/mail/MailReservationService.kt diff --git a/src/main/kotlin/apply/application/mail/MailDtos.kt b/src/main/kotlin/apply/application/mail/MailDtos.kt index 077086850..701609b47 100644 --- a/src/main/kotlin/apply/application/mail/MailDtos.kt +++ b/src/main/kotlin/apply/application/mail/MailDtos.kt @@ -38,3 +38,17 @@ data class MailReservationSimpleResponse( mailReservation.id ) } + +data class MailReservationResponse( + val mailMessage: MailMessageResponse, + val status: MailReservationStatus, + val reservationTime: LocalDateTime, + val id: Long, +) { + constructor(mailReservation: MailReservation) : this( + MailMessageResponse(mailReservation.mailMessage), + mailReservation.status, + mailReservation.reservationTime, + mailReservation.id + ) +} diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt new file mode 100644 index 000000000..989ea458a --- /dev/null +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -0,0 +1,21 @@ +package apply.application.mail + +import apply.domain.mail.MailReservationRepository +import apply.domain.mail.getOrThrow +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Transactional +@Service +class MailReservationService( + private val mailReservationRepository: MailReservationRepository, +) { + fun updateReservationTime(mailReservationId: Long, reservationTime: LocalDateTime): MailReservationResponse { + val mailReservation = mailReservationRepository.getOrThrow(mailReservationId).apply { + update(reservationTime) + } + + return MailReservationResponse(mailReservationRepository.save(mailReservation)) + } +} diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index a5c5f56bc..7e657574b 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -30,19 +30,12 @@ class MailReservation( val creatorId: Long, @Column(nullable = false) - val reservationTime: LocalDateTime, + var reservationTime: LocalDateTime, id: Long = 0L ) : BaseEntity(id) { init { - require(reservationTime.isAfter(LocalDateTime.now())) { - "예약 메일의 예약시간은 현재시간보다 늦은 시간이어야 합니다." - } - - // TODO: validator 추출 - require(reservationTime.minute % PERIOD_MINUTES == 0L) { - "예약 메일의 예약시간은 10분 단위로 설정해야 합니다." - } + validateTime(reservationTime) } fun process() { @@ -53,11 +46,29 @@ class MailReservation( status = MailReservationStatus.FINISHED } + fun update(reservationTime: LocalDateTime) { + validateTime(reservationTime) + validateStatus() + + this.reservationTime = reservationTime + } + fun validateStatus() { check(status == MailReservationStatus.WAITING) { "메일 예약은 WAITING 상태에서만 가능합니다." } } + + private fun validateTime(reservationTime: LocalDateTime) { + require(reservationTime.isAfter(LocalDateTime.now())) { + "예약 메일의 예약시간은 현재시간보다 늦은 시간이어야 합니다." + } + + // TODO: validator 추출 + require(reservationTime.minute % PERIOD_MINUTES == 0L) { + "예약 메일의 예약시간은 10분 단위로 설정해야 합니다." + } + } } enum class MailReservationStatus { diff --git a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt index 64e686438..31bc71a0d 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt @@ -1,5 +1,9 @@ package apply.domain.mail import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun MailReservationRepository.getOrThrow(id: Long) = findByIdOrNull(id) + ?: throw NoSuchElementException("메일 예약이 존재하지 않습니다. id: $id") interface MailReservationRepository : JpaRepository diff --git a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt index 36638f412..706498268 100644 --- a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt @@ -53,4 +53,24 @@ class MailReservationTest : StringSpec({ mailReservation.complete() mailReservation.status shouldBe MailReservationStatus.FINISHED } + + "예약 메일의 예약 시간을 변경한다" { + val mailReservation = createMailReservation() + + val updatedReservationTime = now().plusHours(1).withMinute(0) + mailReservation.update(updatedReservationTime) + + mailReservation.reservationTime shouldBe updatedReservationTime + } + + "예약 메일 전송이 시작거나 완료 되었다면 예약 시간을 변경할 수 없다" { + listOf( + createMailReservation().apply { process() }, + createMailReservation().apply { complete() } + ).forAll { mailReservation -> + shouldThrow { + mailReservation.update(now().plusHours(1).withMinute(0)) + } + } + } }) From f5777a476ffec28893ab0736828c8c3069dd5c42 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 08:14:15 +0900 Subject: [PATCH 11/40] feat: add functionality for deleting mail's reservation --- .../mail/MailReservationService.kt | 6 +++ .../apply/domain/mail/MailReservation.kt | 2 +- .../mail/MailReservationServiceTest.kt | 47 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index 989ea458a..f57f3e319 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -18,4 +18,10 @@ class MailReservationService( return MailReservationResponse(mailReservationRepository.save(mailReservation)) } + + fun deleteReservation(mailReservationId: Long) { + val mailReservation = mailReservationRepository.getOrThrow(mailReservationId) + mailReservation.validateStatus() + mailReservationRepository.deleteById(mailReservation.id) + } } diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index 7e657574b..2b08bcfff 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -55,7 +55,7 @@ class MailReservation( fun validateStatus() { check(status == MailReservationStatus.WAITING) { - "메일 예약은 WAITING 상태에서만 가능합니다." + "메일 예약 변경/삭제는 WAITING 상태에서만 가능합니다." } } diff --git a/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt b/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt new file mode 100644 index 000000000..5d60bbc73 --- /dev/null +++ b/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt @@ -0,0 +1,47 @@ +package apply.application.mail + +import apply.createMailReservation +import apply.domain.mail.MailReservationRepository +import apply.domain.mail.getOrThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.inspectors.forAll +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk + +class MailReservationServiceTest : BehaviorSpec({ + val mailReservationRepository = mockk() + val mailReservationService = MailReservationService(mailReservationRepository) + + Given("발송 전 상태의 에약 메일이 있는 경우") { + val mailReservation = createMailReservation() + + every { mailReservationRepository.getOrThrow(any()) } returns mailReservation + every { mailReservationRepository.deleteById(any()) } just Runs + + When("해당 메일 예약 삭제를 요청하면") { + Then("메일 예약이 삭제된다") { + mailReservationService.deleteReservation(mailReservation.id) + } + } + } + + Given("예약 발송이 진행중이거나 완료 상태의 에약 메일이 있는 경우") { + listOf( + createMailReservation().apply { complete() }, + createMailReservation().apply { process() } + ).forAll { mailReservation -> + every { mailReservationRepository.getOrThrow(any()) } returns mailReservation + + When("해당 메일 예약 삭제를 요청하면") { + Then("예외가 발생한다") { + shouldThrow { + mailReservationService.deleteReservation(mailReservation.id) + } + } + } + } + } +}) From 9e29d51bbb9f868aed423c0cb6085794f1072821 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 08:35:47 +0900 Subject: [PATCH 12/40] feat: add functionality for finding mail's reservation --- .../kotlin/apply/application/mail/MailReservationService.kt | 6 ++++++ .../kotlin/apply/domain/mail/MailReservationRepository.kt | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index f57f3e319..48c7ba216 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -1,6 +1,7 @@ package apply.application.mail import apply.domain.mail.MailReservationRepository +import apply.domain.mail.MailReservationStatus import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -11,6 +12,11 @@ import java.time.LocalDateTime class MailReservationService( private val mailReservationRepository: MailReservationRepository, ) { + fun findByWaitingStatus(): List { + return mailReservationRepository.findByStatus(MailReservationStatus.WAITING) + .map { MailReservationResponse(it) } + } + fun updateReservationTime(mailReservationId: Long, reservationTime: LocalDateTime): MailReservationResponse { val mailReservation = mailReservationRepository.getOrThrow(mailReservationId).apply { update(reservationTime) diff --git a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt index 31bc71a0d..c814c30a9 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt @@ -6,4 +6,6 @@ import org.springframework.data.repository.findByIdOrNull fun MailReservationRepository.getOrThrow(id: Long) = findByIdOrNull(id) ?: throw NoSuchElementException("메일 예약이 존재하지 않습니다. id: $id") -interface MailReservationRepository : JpaRepository +interface MailReservationRepository : JpaRepository { + fun findByStatus(status: MailReservationStatus): List +} From 501928e700c0c017b59d738b2aaf8823d9fdccf6 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 09:09:29 +0900 Subject: [PATCH 13/40] refactor: create MailHistory when MailSentEvent is published --- .../apply/application/MailHistoryService.kt | 22 ---------- .../application/mail/MailHistoryService.kt | 43 +++++++++++++++++++ .../apply/application/mail/MailService.kt | 23 +++------- .../kotlin/apply/domain/mail/MailSentEvent.kt | 9 ++++ .../apply/ui/admin/mail/MailsFormView.kt | 2 +- .../kotlin/apply/ui/admin/mail/MailsView.kt | 2 +- .../apply/ui/api/MailHistoryRestController.kt | 2 +- .../application/MailHistoryServiceTest.kt | 5 ++- .../ui/api/MailHistoryRestControllerTest.kt | 2 +- 9 files changed, 65 insertions(+), 45 deletions(-) delete mode 100644 src/main/kotlin/apply/application/MailHistoryService.kt create mode 100644 src/main/kotlin/apply/application/mail/MailHistoryService.kt create mode 100644 src/main/kotlin/apply/domain/mail/MailSentEvent.kt diff --git a/src/main/kotlin/apply/application/MailHistoryService.kt b/src/main/kotlin/apply/application/MailHistoryService.kt deleted file mode 100644 index a9ff4b9bb..000000000 --- a/src/main/kotlin/apply/application/MailHistoryService.kt +++ /dev/null @@ -1,22 +0,0 @@ -package apply.application - -import apply.application.mail.MailData -import apply.domain.mail.MailHistoryRepository -import apply.domain.mail.getOrThrow -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -@Transactional -@Service -class MailHistoryService( - private val mailHistoryRepository: MailHistoryRepository -) { - fun findAll(): List { - return mailHistoryRepository.findAll().map { MailData(it) } - } - - fun getById(mailHistoryId: Long): MailData { - val mailHistory = mailHistoryRepository.getOrThrow(mailHistoryId) - return MailData(mailHistory) - } -} diff --git a/src/main/kotlin/apply/application/mail/MailHistoryService.kt b/src/main/kotlin/apply/application/mail/MailHistoryService.kt new file mode 100644 index 000000000..94e5df492 --- /dev/null +++ b/src/main/kotlin/apply/application/mail/MailHistoryService.kt @@ -0,0 +1,43 @@ +package apply.application.mail + +import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.MailMessageRepository +import apply.domain.mail.MailSentEvent +import apply.domain.mail.getOrThrow +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class MailHistoryService( + private val mailHistoryRepository: MailHistoryRepository, + private val mailMessageRepository: MailMessageRepository +) { + @EventListener + fun onMailSentEvent(event: MailSentEvent) { + val mailData = event.mailData + val mailMessage = mailMessageRepository.findById(mailData.id) + .let { mailMessageRepository.save(mailData.toMailMessage()) } + + val mailHistories = mutableListOf() + if (event.succeedRecipients.isNotEmpty()) { + mailHistories.add(MailHistory.ofSuccess(mailMessage, event.succeedRecipients)) + } + if (event.failedRecipients.isNotEmpty()) { + mailHistories.add(MailHistory.ofFailure(mailMessage, event.failedRecipients)) + } + + mailHistoryRepository.saveAll(mailHistories) + } + + fun findAll(): List { + return mailHistoryRepository.findAll().map { MailData(it) } + } + + fun getById(mailHistoryId: Long): MailData { + val mailHistory = mailHistoryRepository.getOrThrow(mailHistoryId) + return MailData(mailHistory) + } +} diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/MailService.kt index a8f25d3ab..fbbb68817 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/MailService.kt @@ -2,15 +2,14 @@ package apply.application.mail import apply.application.ApplicationProperties import apply.domain.applicationform.ApplicationFormSubmittedEvent -import apply.domain.mail.MailHistory -import apply.domain.mail.MailHistoryRepository -import apply.domain.mail.MailMessageRepository +import apply.domain.mail.MailSentEvent import apply.domain.recruitment.RecruitmentRepository import apply.domain.recruitment.getOrThrow import apply.domain.user.PasswordResetEvent import apply.domain.user.UserRepository import apply.domain.user.getOrThrow import org.springframework.boot.autoconfigure.mail.MailProperties +import org.springframework.context.ApplicationEventPublisher import org.springframework.core.io.ByteArrayResource import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @@ -25,12 +24,11 @@ private const val MAIL_SENDING_UNIT: Int = 50 class MailService( private val userRepository: UserRepository, private val recruitmentRepository: RecruitmentRepository, - private val mailMessageRepository: MailMessageRepository, - private val mailHistoryRepository: MailHistoryRepository, private val applicationProperties: ApplicationProperties, private val templateEngine: ISpringTemplateEngine, private val mailSender: MailSender, - private val mailProperties: MailProperties + private val mailProperties: MailProperties, + private val eventPublisher: ApplicationEventPublisher, ) { @Async @TransactionalEventListener @@ -92,7 +90,6 @@ class MailService( @Async fun sendMailsByBcc(request: MailData, files: Map) { - val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username @@ -104,17 +101,7 @@ class MailService( .onFailure { failed.addAll(addresses) } } - val mailHistories = mutableListOf() - - if (succeeded.isNotEmpty()) { - mailHistories.add(MailHistory.ofSuccess(mailMessage, succeeded)) - } - - if (failed.isNotEmpty()) { - mailHistories.add(MailHistory.ofFailure(mailMessage, failed)) - } - - mailHistoryRepository.saveAll(mailHistories) + eventPublisher.publishEvent(MailSentEvent(request, succeeded, failed)) } fun generateMailBody(mailData: MailData): String { diff --git a/src/main/kotlin/apply/domain/mail/MailSentEvent.kt b/src/main/kotlin/apply/domain/mail/MailSentEvent.kt new file mode 100644 index 000000000..d117573ab --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailSentEvent.kt @@ -0,0 +1,9 @@ +package apply.domain.mail + +import apply.application.mail.MailData + +data class MailSentEvent( + val mailData: MailData, + val succeedRecipients: List, + val failedRecipients: List +) diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt index d1ac8ccdb..ab8328275 100644 --- a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt +++ b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt @@ -1,7 +1,7 @@ package apply.ui.admin.mail import apply.application.EvaluationService -import apply.application.MailHistoryService +import apply.application.mail.MailHistoryService import apply.application.MailTargetService import apply.application.RecruitmentService import apply.application.UserService diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsView.kt index 8864343c6..6310918a0 100644 --- a/src/main/kotlin/apply/ui/admin/mail/MailsView.kt +++ b/src/main/kotlin/apply/ui/admin/mail/MailsView.kt @@ -1,6 +1,6 @@ package apply.ui.admin.mail -import apply.application.MailHistoryService +import apply.application.mail.MailHistoryService import apply.application.mail.MailData import apply.ui.admin.BaseLayout import com.vaadin.flow.component.Component diff --git a/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt b/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt index 7b84a6060..3ecad4465 100644 --- a/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt @@ -1,6 +1,6 @@ package apply.ui.api -import apply.application.MailHistoryService +import apply.application.mail.MailHistoryService import apply.application.mail.MailData import apply.domain.user.User import apply.security.LoginUser diff --git a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt index d6a5b35f4..bddf7e607 100644 --- a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt +++ b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt @@ -1,7 +1,9 @@ package apply.application +import apply.application.mail.MailHistoryService import apply.createSuccessMailHistory2 import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.MailMessageRepository import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe import io.mockk.clearAllMocks @@ -11,8 +13,9 @@ import support.test.spec.afterRootTest class MailHistoryServiceTest : BehaviorSpec({ val mailHistoryRepository = mockk() + val mailMessageRepository = mockk() - val mailHistoryService = MailHistoryService(mailHistoryRepository) + val mailHistoryService = MailHistoryService(mailHistoryRepository, mailMessageRepository) Given("메일 이력이 있는 경우") { every { mailHistoryRepository.findAll() } returns listOf( diff --git a/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt b/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt index 0d5940e34..ef8c604a3 100644 --- a/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt +++ b/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt @@ -1,6 +1,6 @@ package apply.ui.api -import apply.application.MailHistoryService +import apply.application.mail.MailHistoryService import apply.createMailData import com.ninjasquad.springmockk.MockkBean import io.mockk.every From e914ed2c3ab0241c4db51182a9a5611d11b0966c Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 15:10:11 +0900 Subject: [PATCH 14/40] feat: add endpoint to send reservation mail --- .../kotlin/apply/application/mail/MailData.kt | 8 +++++++ .../mail/MailReservationService.kt | 16 +++++++++++++ .../apply/application/mail/MailService.kt | 3 ++- .../domain/mail/MailReservationRepository.kt | 6 +++++ .../ui/api/MailReservationRestController.kt | 23 +++++++++++++++++++ .../mail/MailReservationServiceTest.kt | 3 ++- 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/apply/ui/api/MailReservationRestController.kt diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index 9d325d604..8f54e0d84 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -40,6 +40,14 @@ data class MailData( id = mailHistory.id ) + constructor(mailMessage: MailMessage) : this( + mailMessage.subject, + mailMessage.body, + mailMessage.sender, + mailMessage.recipients, + id = mailMessage.id + ) + fun toMailMessage(): MailMessage { // TODO: 작성자 ID 바인딩 return MailMessage.of(subject, body, sender, recipients, 1L) diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index 48c7ba216..9b4bfb111 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -10,6 +10,7 @@ import java.time.LocalDateTime @Transactional @Service class MailReservationService( + private val mailService: MailService, private val mailReservationRepository: MailReservationRepository, ) { fun findByWaitingStatus(): List { @@ -30,4 +31,19 @@ class MailReservationService( mailReservation.validateStatus() mailReservationRepository.deleteById(mailReservation.id) } + + fun sendMail(standardTime: LocalDateTime = LocalDateTime.now()) { + val reservations = mailReservationRepository.findByReservationTimeBetweenAndStatus( + standardTime.withMinute(1), + standardTime.plusMinutes(1), + MailReservationStatus.WAITING + ) + + reservations.forEach { mailReservation -> + mailReservation.process() + mailService.sendMailsByBcc(MailData(mailReservation.mailMessage), emptyMap()) { + mailReservation.complete() + } + } + } } diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/MailService.kt index fbbb68817..d4daaeece 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/MailService.kt @@ -89,7 +89,7 @@ class MailService( } @Async - fun sendMailsByBcc(request: MailData, files: Map) { + fun sendMailsByBcc(request: MailData, files: Map, afterAction: () -> Unit = {}) { val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username @@ -102,6 +102,7 @@ class MailService( } eventPublisher.publishEvent(MailSentEvent(request, succeeded, failed)) + afterAction() } fun generateMailBody(mailData: MailData): String { diff --git a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt index c814c30a9..b7f53b8cd 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt @@ -2,10 +2,16 @@ package apply.domain.mail import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.repository.findByIdOrNull +import java.time.LocalDateTime fun MailReservationRepository.getOrThrow(id: Long) = findByIdOrNull(id) ?: throw NoSuchElementException("메일 예약이 존재하지 않습니다. id: $id") interface MailReservationRepository : JpaRepository { fun findByStatus(status: MailReservationStatus): List + fun findByReservationTimeBetweenAndStatus( + from: LocalDateTime, + to: LocalDateTime, + status: MailReservationStatus + ): List } diff --git a/src/main/kotlin/apply/ui/api/MailReservationRestController.kt b/src/main/kotlin/apply/ui/api/MailReservationRestController.kt new file mode 100644 index 000000000..ab865ce9d --- /dev/null +++ b/src/main/kotlin/apply/ui/api/MailReservationRestController.kt @@ -0,0 +1,23 @@ +package apply.ui.api + +import apply.application.mail.MailReservationService +import apply.domain.user.User +import apply.security.LoginUser +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/mail-reservation") +class MailReservationRestController( + private val mailReservationService: MailReservationService +) { + @PostMapping + fun sendMail( + @LoginUser(administrator = true) user: User + ): ResponseEntity { + mailReservationService.sendMail() + return ResponseEntity.noContent().build() + } +} diff --git a/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt b/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt index 5d60bbc73..080494b6a 100644 --- a/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt +++ b/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt @@ -12,8 +12,9 @@ import io.mockk.just import io.mockk.mockk class MailReservationServiceTest : BehaviorSpec({ + val mailService = mockk() val mailReservationRepository = mockk() - val mailReservationService = MailReservationService(mailReservationRepository) + val mailReservationService = MailReservationService(mailService, mailReservationRepository) Given("발송 전 상태의 에약 메일이 있는 경우") { val mailReservation = createMailReservation() From f766d5def0c437487650d1ef2625b3cc054b3493 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 5 Oct 2023 15:45:03 +0900 Subject: [PATCH 15/40] refactor: migrate sql relating with mail tables --- .../db/migration/V3_2__Add_mail_history.sql | 15 --------------- .../db/migration/V3_3__rename_mail_history.sql | 3 --- ...ervation.sql => V4__Create_mail_tables.sql} | 18 ++++++++++++++++++ 3 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 src/main/resources/db/migration/V3_2__Add_mail_history.sql delete mode 100644 src/main/resources/db/migration/V3_3__rename_mail_history.sql rename src/main/resources/db/migration/{V3_1__Add_mail_message_and_reservation.sql => V4__Create_mail_tables.sql} (64%) diff --git a/src/main/resources/db/migration/V3_2__Add_mail_history.sql b/src/main/resources/db/migration/V3_2__Add_mail_history.sql deleted file mode 100644 index c61ddf21d..000000000 --- a/src/main/resources/db/migration/V3_2__Add_mail_history.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table mail_history2 -( - id bigint not null auto_increment, - recipients longtext not null, - sent_time datetime(6) not null, - success bit(1) not null, - mail_message_id bigint not null, - primary key (id) -) engine = InnoDB - default charset = utf8mb4; - -alter table mail_history2 - add constraint fk_mail_history_to_mail_message - foreign key (mail_message_id) - references mail_message (id); \ No newline at end of file diff --git a/src/main/resources/db/migration/V3_3__rename_mail_history.sql b/src/main/resources/db/migration/V3_3__rename_mail_history.sql deleted file mode 100644 index 1afec532d..000000000 --- a/src/main/resources/db/migration/V3_3__rename_mail_history.sql +++ /dev/null @@ -1,3 +0,0 @@ -rename table mail_history to mail_history_old; - -rename table mail_history2 to mail_history; diff --git a/src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql b/src/main/resources/db/migration/V4__Create_mail_tables.sql similarity index 64% rename from src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql rename to src/main/resources/db/migration/V4__Create_mail_tables.sql index 70f91f7b8..6c27a7bf2 100644 --- a/src/main/resources/db/migration/V3_1__Add_mail_message_and_reservation.sql +++ b/src/main/resources/db/migration/V4__Create_mail_tables.sql @@ -27,3 +27,21 @@ alter table mail_reservation add constraint fk_mail_reservation_to_mail_message foreign key (mail_message_id) references mail_message (id); + +rename table mail_history to mail_history_old; + +create table mail_history +( + id bigint not null auto_increment, + recipients longtext not null, + sent_time datetime(6) not null, + success bit(1) not null, + mail_message_id bigint not null, + primary key (id) +) engine = InnoDB + default charset = utf8mb4; + +alter table mail_history + add constraint fk_mail_history_to_mail_message + foreign key (mail_message_id) + references mail_message (id); From 3b6cd967d1d02736b9f415b8ce3e5f2520796f20 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Sat, 7 Oct 2023 14:29:02 +0900 Subject: [PATCH 16/40] fix: edit searching conditions for mail reservations --- .../kotlin/apply/application/mail/MailReservationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index 9b4bfb111..1554b9b4e 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -34,7 +34,7 @@ class MailReservationService( fun sendMail(standardTime: LocalDateTime = LocalDateTime.now()) { val reservations = mailReservationRepository.findByReservationTimeBetweenAndStatus( - standardTime.withMinute(1), + standardTime.minusMinutes(1), standardTime.plusMinutes(1), MailReservationStatus.WAITING ) From ba9d7b0ad85e91df7287608e278efd2d07da77af Mon Sep 17 00:00:00 2001 From: woowabrie Date: Sat, 7 Oct 2023 14:29:19 +0900 Subject: [PATCH 17/40] feat: remove functions relates with updating mail message - update functionality is replaced with deleting and creating --- .../kotlin/apply/application/mail/MailDtos.kt | 2 -- .../application/mail/MailMessageService.kt | 7 ----- .../kotlin/apply/domain/mail/MailMessage.kt | 23 +++------------ .../db/migration/V4__Create_mail_tables.sql | 1 - .../mail/MailMessageIntegrationTest.kt | 25 ----------------- .../apply/domain/mail/MailMessageTest.kt | 28 ------------------- 6 files changed, 4 insertions(+), 82 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailDtos.kt b/src/main/kotlin/apply/application/mail/MailDtos.kt index 701609b47..82b93a4bb 100644 --- a/src/main/kotlin/apply/application/mail/MailDtos.kt +++ b/src/main/kotlin/apply/application/mail/MailDtos.kt @@ -11,7 +11,6 @@ data class MailMessageResponse( val sender: String, val recipients: List, val createdDateTime: LocalDateTime, - val modifiedDateTime: LocalDateTime, val reservation: MailReservationSimpleResponse?, val id: Long ) { @@ -21,7 +20,6 @@ data class MailMessageResponse( mailMessage.sender, mailMessage.recipients, mailMessage.createdDateTime, - mailMessage.modifiedDateTime, mailMessage.reservation()?.let { MailReservationSimpleResponse(it) }, mailMessage.id ) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index af2c28202..1eeaf23c0 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -1,7 +1,6 @@ package apply.application.mail import apply.domain.mail.MailMessageRepository -import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -14,10 +13,4 @@ class MailMessageService( val mailMessage = mailMessageRepository.save(request.toReservationMailMessage()) return MailMessageResponse(mailMessage) } - - fun updateReservedMessage(mailMessageId: Long, request: MailData) { - val mailMessage = mailMessageRepository.getOrThrow(mailMessageId) - mailMessage.update(request.subject, request.body, request.recipients) - mailMessageRepository.save(mailMessage) - } } diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index e96e8c0b8..ce1b292ef 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -14,36 +14,31 @@ import javax.persistence.OneToMany @Entity class MailMessage private constructor( @Column(nullable = false) - var subject: String, + val subject: String, @Column(nullable = false) @Lob - var body: String, + val body: String, @Column(nullable = false) - var sender: String, + val sender: String, @Column(nullable = false) @Convert(converter = StringToListConverter::class) @Lob - var recipients: List, + val recipients: List, @Column(nullable = false) val creatorId: Long, @Column(nullable = false) val createdDateTime: LocalDateTime = LocalDateTime.now(), - modifiedDateTime: LocalDateTime = LocalDateTime.now(), id: Long = 0L ) : BaseEntity(id) { @OneToMany(mappedBy = "mailMessage", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) var reservations: MutableList = mutableListOf() - @Column(nullable = false) - var modifiedDateTime: LocalDateTime = modifiedDateTime - private set - fun reservation(): MailReservation? { if (!hasReservation()) { return null @@ -52,16 +47,6 @@ class MailMessage private constructor( return reservations.first() } - fun update(subject: String, body: String, recipients: List) { - check(hasReservation()) - reservation()?.validateStatus() - - this.subject = subject - this.body = body - this.recipients = recipients - this.modifiedDateTime = LocalDateTime.now() - } - private fun hasReservation(): Boolean { return reservations.isNotEmpty() } diff --git a/src/main/resources/db/migration/V4__Create_mail_tables.sql b/src/main/resources/db/migration/V4__Create_mail_tables.sql index 6c27a7bf2..8940406fc 100644 --- a/src/main/resources/db/migration/V4__Create_mail_tables.sql +++ b/src/main/resources/db/migration/V4__Create_mail_tables.sql @@ -7,7 +7,6 @@ create table mail_message recipients longtext not null, creator_id bigint not null, created_date_time datetime(6) not null, - modified_date_time datetime(6) not null, primary key (id) ) engine = InnoDB default charset = utf8mb4; diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index c8f8b300c..c7765da33 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -36,29 +36,4 @@ class MailMessageIntegrationTest( } } } - - Given("메일 예약 후 발송 작업이 아직 시작되지 않았을 경우") { - val reservationTime = now().plusHours(3).withMinute(10) - val mailData = createMailData(sentTime = reservationTime) - val mailMessage = mailMessageService.reserve(mailData) - - When("메일 내용을 변경하면") { - val updatedMailData = createMailData( - subject = "변경된 제목", - body = "변경된 본문", - recipients = listOf("변경된 수신자") - ) - mailMessageService.updateReservedMessage(mailMessage.id, updatedMailData) - - Then("변경된 내용이 반영된다") { - val actual = mailMessageRepository.getOrThrow(mailMessage.id) - - actual.subject shouldBe updatedMailData.subject - actual.body shouldBe updatedMailData.body - actual.recipients shouldBe updatedMailData.recipients - actual.reservation().shouldNotBeNull() - actual.reservation()!!.reservationTime shouldBe reservationTime - } - } - } }) diff --git a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt index ebe3778a5..24c7d6b1b 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt @@ -2,7 +2,6 @@ package apply.domain.mail import apply.createMailMessage import apply.createReservationMailMessage -import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -23,31 +22,4 @@ class MailMessageTest : StringSpec({ mailMessage.body shouldBe "내용" mailMessage.reservation() shouldNotBe null } - - "예약 메일 메시지의 내용을 수정한다" { - val mailMessage = createReservationMailMessage(subject = "제목", body = "내용") - - mailMessage.update("수정된 제목", "수정된 내용", mailMessage.recipients) - - mailMessage.subject shouldBe "수정된 제목" - mailMessage.body shouldBe "수정된 내용" - mailMessage.reservation() shouldNotBe null - } - - "일반 메일 메시지의 내용은 수정할 수 없다" { - val mailMessage = createMailMessage() - - shouldThrow { - mailMessage.update("수정된 제목", "수정된 내용", mailMessage.recipients) - } - } - - "발송 이후 상태인 예약 메일 메시지의 내용은 수정할 수 없다" { - val mailMessage = createReservationMailMessage() - mailMessage.reservation()?.process() - - shouldThrow { - mailMessage.update("수정된 제목", "수정된 내용", mailMessage.recipients) - } - } }) From 16e0a74e23c4e94b499e93f527fc043ad7e44c4f Mon Sep 17 00:00:00 2001 From: woowabrie Date: Sat, 7 Oct 2023 15:15:28 +0900 Subject: [PATCH 18/40] feat: remove functions relates with updating reservationTime of mail - update functionality is replaced with deleting and creating --- .../application/mail/MailMessageService.kt | 7 +++ .../mail/MailReservationService.kt | 15 ------ .../kotlin/apply/domain/mail/MailMessage.kt | 6 ++- .../apply/domain/mail/MailReservation.kt | 18 ++----- .../db/migration/V4__Create_mail_tables.sql | 1 - .../kotlin/apply/MailHistoryFixtures.kt.kt | 3 +- .../mail/MailMessageIntegrationTest.kt | 53 ++++++++++++++++++- .../mail/MailReservationServiceTest.kt | 48 ----------------- .../apply/domain/mail/MailReservationTest.kt | 20 ------- 9 files changed, 68 insertions(+), 103 deletions(-) delete mode 100644 src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 1eeaf23c0..b49f3aedd 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -1,6 +1,7 @@ package apply.application.mail import apply.domain.mail.MailMessageRepository +import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -13,4 +14,10 @@ class MailMessageService( val mailMessage = mailMessageRepository.save(request.toReservationMailMessage()) return MailMessageResponse(mailMessage) } + + fun cancelReservation(mailMessageId: Long) { + val mailMessage = mailMessageRepository.getOrThrow(mailMessageId) + check(mailMessage.canDelete()) { "예약 취소할 수 없는 메일입니다." } + mailMessageRepository.deleteById(mailMessageId) + } } diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index 1554b9b4e..80ef2d8e8 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -2,7 +2,6 @@ package apply.application.mail import apply.domain.mail.MailReservationRepository import apply.domain.mail.MailReservationStatus -import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @@ -18,20 +17,6 @@ class MailReservationService( .map { MailReservationResponse(it) } } - fun updateReservationTime(mailReservationId: Long, reservationTime: LocalDateTime): MailReservationResponse { - val mailReservation = mailReservationRepository.getOrThrow(mailReservationId).apply { - update(reservationTime) - } - - return MailReservationResponse(mailReservationRepository.save(mailReservation)) - } - - fun deleteReservation(mailReservationId: Long) { - val mailReservation = mailReservationRepository.getOrThrow(mailReservationId) - mailReservation.validateStatus() - mailReservationRepository.deleteById(mailReservation.id) - } - fun sendMail(standardTime: LocalDateTime = LocalDateTime.now()) { val reservations = mailReservationRepository.findByReservationTimeBetweenAndStatus( standardTime.minusMinutes(1), diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index ce1b292ef..04e48d396 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -47,6 +47,10 @@ class MailMessage private constructor( return reservations.first() } + fun canDelete(): Boolean { + return this.reservation()?.canCancel() ?: true + } + private fun hasReservation(): Boolean { return reservations.isNotEmpty() } @@ -84,7 +88,7 @@ class MailMessage private constructor( creatorId = creatorId ).apply { reservations.add( - MailReservation(this, creatorId = creatorId, reservationTime = reservationTime) + MailReservation(this, reservationTime = reservationTime) ) } } diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index 2b08bcfff..f48f610da 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -27,10 +27,7 @@ class MailReservation( var status: MailReservationStatus = MailReservationStatus.WAITING, @Column(nullable = false) - val creatorId: Long, - - @Column(nullable = false) - var reservationTime: LocalDateTime, + val reservationTime: LocalDateTime, id: Long = 0L ) : BaseEntity(id) { @@ -46,17 +43,8 @@ class MailReservation( status = MailReservationStatus.FINISHED } - fun update(reservationTime: LocalDateTime) { - validateTime(reservationTime) - validateStatus() - - this.reservationTime = reservationTime - } - - fun validateStatus() { - check(status == MailReservationStatus.WAITING) { - "메일 예약 변경/삭제는 WAITING 상태에서만 가능합니다." - } + fun canCancel(): Boolean { + return status == MailReservationStatus.WAITING } private fun validateTime(reservationTime: LocalDateTime) { diff --git a/src/main/resources/db/migration/V4__Create_mail_tables.sql b/src/main/resources/db/migration/V4__Create_mail_tables.sql index 8940406fc..7a58671f9 100644 --- a/src/main/resources/db/migration/V4__Create_mail_tables.sql +++ b/src/main/resources/db/migration/V4__Create_mail_tables.sql @@ -16,7 +16,6 @@ create table mail_reservation id bigint not null auto_increment, status varchar(8) not null, mail_message_id bigint not null, - creator_id bigint not null, reservation_time datetime(6) not null, primary key (id) ) engine = InnoDB diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index 64a20e53c..dec3c4212 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -58,8 +58,7 @@ fun createMailReservation( ): MailReservation { return MailReservation( createMailMessage(subject, body, sender, recipients, id, createId), - reservationTime = reservationTime, - creatorId = createId + reservationTime = reservationTime ) } diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index c7765da33..fdbea0742 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -1,13 +1,16 @@ package apply.application.mail import apply.createMailData +import apply.createReservationMailMessage import apply.domain.mail.MailMessageRepository import apply.domain.mail.getOrThrow +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.extensions.spring.SpringTestExtension import io.kotest.extensions.spring.SpringTestLifecycleMode import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import org.springframework.transaction.annotation.Transactional import support.test.IntegrationTest import java.time.LocalDateTime.now @@ -16,7 +19,8 @@ import java.time.LocalDateTime.now @IntegrationTest class MailMessageIntegrationTest( private val mailMessageService: MailMessageService, - private val mailMessageRepository: MailMessageRepository + private val mailMessageRepository: MailMessageRepository, + private val mailReservationRepository: MailMessageRepository ) : BehaviorSpec({ extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) @@ -36,4 +40,51 @@ class MailMessageIntegrationTest( } } } + + Given("취소하고 싶은 예약 메일이 있는 경우") { + val mailMessage = mailMessageRepository.save(createReservationMailMessage()) + + When("메일 예약을 취소하면") { + mailMessageService.cancelReservation(mailMessage.id) + + Then("메일 메시지와 예약이 삭제된다") { + mailMessageRepository.findById(mailMessage.id).isEmpty shouldBe true + mailReservationRepository.findAll() shouldBe emptyList() + } + } + } + + Given("취소하고 싶은 예약 메일이 처리중인 경우") { + val mailMessage = mailMessageRepository.save(createReservationMailMessage()) + mailMessage.reservation()?.process() + + When("메일 예약을 취소하면") { + Then("에러가 발생하고 메일 메시지와 예약은 남아있다") { + shouldThrow { + mailMessageService.cancelReservation(mailMessage.id) + } + + val actual = mailMessageRepository.getOrThrow(mailMessage.id) + actual shouldNotBe null + actual.reservation() shouldNotBe null + } + } + } + + Given("취소하고 싶은 예약 메일이 발송 완료된 경우") { + val mailMessage = mailMessageRepository.save(createReservationMailMessage()) + mailMessage.reservation()?.complete() + + When("메일 예약을 취소하면") { + Then("에러가 발생하고 메일 메시지와 예약은 남아있다") { + shouldThrow { + mailMessageService.cancelReservation(mailMessage.id) + } + + val actual = mailMessageRepository.getOrThrow(mailMessage.id) + actual shouldNotBe null + actual.reservation() shouldNotBe null + } + } + } }) diff --git a/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt b/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt deleted file mode 100644 index 080494b6a..000000000 --- a/src/test/kotlin/apply/application/mail/MailReservationServiceTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package apply.application.mail - -import apply.createMailReservation -import apply.domain.mail.MailReservationRepository -import apply.domain.mail.getOrThrow -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.inspectors.forAll -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk - -class MailReservationServiceTest : BehaviorSpec({ - val mailService = mockk() - val mailReservationRepository = mockk() - val mailReservationService = MailReservationService(mailService, mailReservationRepository) - - Given("발송 전 상태의 에약 메일이 있는 경우") { - val mailReservation = createMailReservation() - - every { mailReservationRepository.getOrThrow(any()) } returns mailReservation - every { mailReservationRepository.deleteById(any()) } just Runs - - When("해당 메일 예약 삭제를 요청하면") { - Then("메일 예약이 삭제된다") { - mailReservationService.deleteReservation(mailReservation.id) - } - } - } - - Given("예약 발송이 진행중이거나 완료 상태의 에약 메일이 있는 경우") { - listOf( - createMailReservation().apply { complete() }, - createMailReservation().apply { process() } - ).forAll { mailReservation -> - every { mailReservationRepository.getOrThrow(any()) } returns mailReservation - - When("해당 메일 예약 삭제를 요청하면") { - Then("예외가 발생한다") { - shouldThrow { - mailReservationService.deleteReservation(mailReservation.id) - } - } - } - } - } -}) diff --git a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt index 706498268..36638f412 100644 --- a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt @@ -53,24 +53,4 @@ class MailReservationTest : StringSpec({ mailReservation.complete() mailReservation.status shouldBe MailReservationStatus.FINISHED } - - "예약 메일의 예약 시간을 변경한다" { - val mailReservation = createMailReservation() - - val updatedReservationTime = now().plusHours(1).withMinute(0) - mailReservation.update(updatedReservationTime) - - mailReservation.reservationTime shouldBe updatedReservationTime - } - - "예약 메일 전송이 시작거나 완료 되었다면 예약 시간을 변경할 수 없다" { - listOf( - createMailReservation().apply { process() }, - createMailReservation().apply { complete() } - ).forAll { mailReservation -> - shouldThrow { - mailReservation.update(now().plusHours(1).withMinute(0)) - } - } - } }) From e2d7e91716c6d064e5863a9e8eb9b1885b485556 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Mon, 9 Oct 2023 09:28:34 +0900 Subject: [PATCH 19/40] refactor: extract MailReservationStatus into another file --- src/main/kotlin/apply/domain/mail/MailReservation.kt | 4 ---- src/main/kotlin/apply/domain/mail/MailReservationStatus.kt | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/apply/domain/mail/MailReservationStatus.kt diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index f48f610da..945e74d70 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -58,7 +58,3 @@ class MailReservation( } } } - -enum class MailReservationStatus { - WAITING, SENDING, FINISHED -} diff --git a/src/main/kotlin/apply/domain/mail/MailReservationStatus.kt b/src/main/kotlin/apply/domain/mail/MailReservationStatus.kt new file mode 100644 index 000000000..800148da2 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailReservationStatus.kt @@ -0,0 +1,5 @@ +package apply.domain.mail + +enum class MailReservationStatus { + WAITING, SENDING, FINISHED +} From 0a673d4fd6844cafeea2204e64e1a82fecea8d69 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Mon, 9 Oct 2023 09:31:50 +0900 Subject: [PATCH 20/40] refactor: adjust order of annotation on MailReservationController --- src/main/kotlin/apply/ui/api/MailReservationRestController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/apply/ui/api/MailReservationRestController.kt b/src/main/kotlin/apply/ui/api/MailReservationRestController.kt index ab865ce9d..66a0daaab 100644 --- a/src/main/kotlin/apply/ui/api/MailReservationRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailReservationRestController.kt @@ -8,8 +8,8 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -@RestController @RequestMapping("/api/mail-reservation") +@RestController class MailReservationRestController( private val mailReservationService: MailReservationService ) { From ee17685607801bd14c8e7b5abf133bd972f280e3 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Mon, 9 Oct 2023 09:42:52 +0900 Subject: [PATCH 21/40] refactor: rename functions changing status of reservation --- .../kotlin/apply/application/mail/MailReservationService.kt | 4 ++-- src/main/kotlin/apply/domain/mail/MailReservation.kt | 4 ++-- .../apply/application/mail/MailMessageIntegrationTest.kt | 4 ++-- src/test/kotlin/apply/domain/mail/MailReservationTest.kt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index 80ef2d8e8..48d763f76 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -25,9 +25,9 @@ class MailReservationService( ) reservations.forEach { mailReservation -> - mailReservation.process() + mailReservation.send() mailService.sendMailsByBcc(MailData(mailReservation.mailMessage), emptyMap()) { - mailReservation.complete() + mailReservation.finish() } } } diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index 945e74d70..a59d319f5 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -35,11 +35,11 @@ class MailReservation( validateTime(reservationTime) } - fun process() { + fun send() { status = MailReservationStatus.SENDING } - fun complete() { + fun finish() { status = MailReservationStatus.FINISHED } diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index fdbea0742..af3513e58 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -56,7 +56,7 @@ class MailMessageIntegrationTest( Given("취소하고 싶은 예약 메일이 처리중인 경우") { val mailMessage = mailMessageRepository.save(createReservationMailMessage()) - mailMessage.reservation()?.process() + mailMessage.reservation()?.send() When("메일 예약을 취소하면") { Then("에러가 발생하고 메일 메시지와 예약은 남아있다") { @@ -73,7 +73,7 @@ class MailMessageIntegrationTest( Given("취소하고 싶은 예약 메일이 발송 완료된 경우") { val mailMessage = mailMessageRepository.save(createReservationMailMessage()) - mailMessage.reservation()?.complete() + mailMessage.reservation()?.finish() When("메일 예약을 취소하면") { Then("에러가 발생하고 메일 메시지와 예약은 남아있다") { diff --git a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt index 36638f412..8b409642c 100644 --- a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt @@ -43,14 +43,14 @@ class MailReservationTest : StringSpec({ "예약 메일 전송을 시작한다" { val mailReservation = createMailReservation() - mailReservation.process() + mailReservation.send() mailReservation.status shouldBe MailReservationStatus.SENDING } "예약 메일 전송을 완료한다" { val mailReservation = createMailReservation() - mailReservation.complete() + mailReservation.finish() mailReservation.status shouldBe MailReservationStatus.FINISHED } }) From 1d11f1be2acd16bde7a94f49dd48f9a6d2fdadc8 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Mon, 9 Oct 2023 13:30:18 +0900 Subject: [PATCH 22/40] refactor: delete factory method from MailHistory --- .../apply/application/mail/MailHistoryService.kt | 4 ++-- .../kotlin/apply/config/DatabaseInitializer.kt | 10 ++++++---- src/main/kotlin/apply/domain/mail/MailHistory.kt | 14 ++------------ src/test/kotlin/apply/MailHistoryFixtures.kt.kt | 4 ++-- .../apply/application/MailHistoryServiceTest.kt | 6 +++--- .../apply/domain/mail/MailHistoryRepositoryTest.kt | 4 ++-- 6 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailHistoryService.kt b/src/main/kotlin/apply/application/mail/MailHistoryService.kt index 94e5df492..fa73669ee 100644 --- a/src/main/kotlin/apply/application/mail/MailHistoryService.kt +++ b/src/main/kotlin/apply/application/mail/MailHistoryService.kt @@ -23,10 +23,10 @@ class MailHistoryService( val mailHistories = mutableListOf() if (event.succeedRecipients.isNotEmpty()) { - mailHistories.add(MailHistory.ofSuccess(mailMessage, event.succeedRecipients)) + mailHistories.add(MailHistory(mailMessage, event.succeedRecipients, true)) } if (event.failedRecipients.isNotEmpty()) { - mailHistories.add(MailHistory.ofFailure(mailMessage, event.failedRecipients)) + mailHistories.add(MailHistory(mailMessage, event.failedRecipients, false)) } mailHistoryRepository.saveAll(mailHistories) diff --git a/src/main/kotlin/apply/config/DatabaseInitializer.kt b/src/main/kotlin/apply/config/DatabaseInitializer.kt index a1432e446..eb123af38 100644 --- a/src/main/kotlin/apply/config/DatabaseInitializer.kt +++ b/src/main/kotlin/apply/config/DatabaseInitializer.kt @@ -442,13 +442,15 @@ class DatabaseInitializer( mailMessageRepository.save(mailMessage) val mailHistories = listOf( - MailHistory.ofSuccess( + MailHistory( mailMessage = mailMessage, - recipients = mailMessage.recipients.subList(0, 2) + recipients = mailMessage.recipients.subList(0, 2), + true ), - MailHistory.ofSuccess( + MailHistory( mailMessage = mailMessage, - recipients = mailMessage.recipients.subList(3, 4) + recipients = mailMessage.recipients.subList(3, 4), + true ) ) mailHistoryRepository.saveAll(mailHistories) diff --git a/src/main/kotlin/apply/domain/mail/MailHistory.kt b/src/main/kotlin/apply/domain/mail/MailHistory.kt index 7178006ff..7caf2a9c5 100644 --- a/src/main/kotlin/apply/domain/mail/MailHistory.kt +++ b/src/main/kotlin/apply/domain/mail/MailHistory.kt @@ -12,7 +12,7 @@ import javax.persistence.Lob import javax.persistence.ManyToOne @Entity -class MailHistory private constructor( +class MailHistory( @ManyToOne @JoinColumn(nullable = false, foreignKey = ForeignKey(name = "fk_mail_history_to_mail_message")) val mailMessage: MailMessage, @@ -28,14 +28,4 @@ class MailHistory private constructor( @Column(nullable = false) val sentTime: LocalDateTime = LocalDateTime.now(), id: Long = 0L -) : BaseEntity(id) { - companion object { - fun ofSuccess(mailMessage: MailMessage, recipients: List): MailHistory { - return MailHistory(mailMessage, recipients, true) - } - - fun ofFailure(mailMessage: MailMessage, recipients: List): MailHistory { - return MailHistory(mailMessage, recipients, false) - } - } -} +) : BaseEntity(id) diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index dec3c4212..a9c2bfa04 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -62,11 +62,11 @@ fun createMailReservation( ) } -fun createSuccessMailHistory2( +fun createSuccessMailHistory( subject: String = SUBJECT, body: String = BODY, sender: String = SENDER, recipients: List = RECIPIENTS ): MailHistory { - return MailHistory.ofSuccess(createMailMessage(subject, body, sender, recipients), recipients) + return MailHistory(createMailMessage(subject, body, sender, recipients), recipients, true) } diff --git a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt index bddf7e607..ee3f818bd 100644 --- a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt +++ b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt @@ -1,7 +1,7 @@ package apply.application import apply.application.mail.MailHistoryService -import apply.createSuccessMailHistory2 +import apply.createSuccessMailHistory import apply.domain.mail.MailHistoryRepository import apply.domain.mail.MailMessageRepository import io.kotest.core.spec.style.BehaviorSpec @@ -19,8 +19,8 @@ class MailHistoryServiceTest : BehaviorSpec({ Given("메일 이력이 있는 경우") { every { mailHistoryRepository.findAll() } returns listOf( - createSuccessMailHistory2(subject = "제목1"), - createSuccessMailHistory2(subject = "제목2") + createSuccessMailHistory(subject = "제목1"), + createSuccessMailHistory(subject = "제목2") ) When("모든 메일 이력을 조회하면") { diff --git a/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt index a576f8661..896f0079a 100644 --- a/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt @@ -21,14 +21,14 @@ class MailHistoryRepositoryTest( val mailMessage = mailMessageRepository.save(createMailMessage()) expect("메일 발송 성공에 대한 히스토리를 저장한다") { - val actual = mailHistoryRepository.save(MailHistory.ofSuccess(mailMessage, mailMessage.recipients)) + val actual = mailHistoryRepository.save(MailHistory(mailMessage, mailMessage.recipients, true)) actual.id.shouldNotBeZero() } } context("메일 히스토리 조회") { val mailMessage = mailMessageRepository.save(createMailMessage()) - mailHistoryRepository.save(MailHistory.ofSuccess(mailMessage, mailMessage.recipients)) + mailHistoryRepository.save(MailHistory(mailMessage, mailMessage.recipients, true)) expect("메일 발송 성공에 대한 히스토리를 저장한다") { val actual = mailHistoryRepository.findAll() From 2c3341ff7cd8adca219e868d737291acb39c3ca4 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Sat, 7 Oct 2023 15:49:01 +0900 Subject: [PATCH 23/40] test: edit typo for creatorId of MailMessage --- src/test/kotlin/apply/MailHistoryFixtures.kt.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index a9c2bfa04..04de978b8 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -30,9 +30,9 @@ fun createMailMessage( sender: String = SENDER, recipients: List = RECIPIENTS, id: Long = 0L, - createId: Long = 0L + creatorId: Long = 0L ): MailMessage { - return MailMessage.of(subject, body, sender, recipients, createId) + return MailMessage.of(subject, body, sender, recipients, creatorId) } fun createReservationMailMessage( @@ -42,9 +42,9 @@ fun createReservationMailMessage( recipients: List = RECIPIENTS, reservationTime: LocalDateTime = RESERVATION_TIME, id: Long = 0L, - createId: Long = 0L + creatorId: Long = 0L ): MailMessage { - return MailMessage.withReservation(subject, body, sender, recipients, reservationTime, createId) + return MailMessage.withReservation(subject, body, sender, recipients, reservationTime, creatorId) } fun createMailReservation( @@ -54,10 +54,10 @@ fun createMailReservation( recipients: List = RECIPIENTS, reservationTime: LocalDateTime = RESERVATION_TIME, id: Long = 0L, - createId: Long = 0L + creatorId: Long = 0L ): MailReservation { return MailReservation( - createMailMessage(subject, body, sender, recipients, id, createId), + createMailMessage(subject, body, sender, recipients, id, creatorId), reservationTime = reservationTime ) } From f0257c5a3d8969c85f5f494d7e03d0eec8e5b18f Mon Sep 17 00:00:00 2001 From: woowabrie Date: Mon, 9 Oct 2023 14:18:49 +0900 Subject: [PATCH 24/40] refactor: delete factory method from MailMessage --- .../kotlin/apply/application/mail/MailData.kt | 7 +--- .../application/mail/MailMessageService.kt | 10 ++++- .../apply/config/DatabaseInitializer.kt | 2 +- .../kotlin/apply/domain/mail/MailMessage.kt | 41 +------------------ .../kotlin/apply/MailHistoryFixtures.kt.kt | 25 ++--------- .../mail/MailMessageIntegrationTest.kt | 31 +++++++------- .../domain/mail/MailMessageRepositoryTest.kt | 22 ---------- .../apply/domain/mail/MailMessageTest.kt | 10 ----- 8 files changed, 31 insertions(+), 117 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index 8f54e0d84..6106cbec9 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -50,11 +50,6 @@ data class MailData( fun toMailMessage(): MailMessage { // TODO: 작성자 ID 바인딩 - return MailMessage.of(subject, body, sender, recipients, 1L) - } - - fun toReservationMailMessage(): MailMessage { - // TODO: 작성자 ID 바인딩 - return MailMessage.withReservation(subject, body, sender, recipients, sentTime, 1L) + return MailMessage(subject, body, sender, recipients, 1L) } } diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index b49f3aedd..9f0946b8a 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -1,6 +1,8 @@ package apply.application.mail import apply.domain.mail.MailMessageRepository +import apply.domain.mail.MailReservation +import apply.domain.mail.MailReservationRepository import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -8,10 +10,14 @@ import org.springframework.transaction.annotation.Transactional @Transactional @Service class MailMessageService( - private val mailMessageRepository: MailMessageRepository + private val mailMessageRepository: MailMessageRepository, + private val mailReservationRepository: MailReservationRepository, ) { fun reserve(request: MailData): MailMessageResponse { - val mailMessage = mailMessageRepository.save(request.toReservationMailMessage()) + val mailMessage = mailMessageRepository.save(request.toMailMessage()) + val mailReservation = mailReservationRepository.save( + MailReservation(mailMessage, reservationTime = request.sentTime) + ) return MailMessageResponse(mailMessage) } diff --git a/src/main/kotlin/apply/config/DatabaseInitializer.kt b/src/main/kotlin/apply/config/DatabaseInitializer.kt index eb123af38..869190c8d 100644 --- a/src/main/kotlin/apply/config/DatabaseInitializer.kt +++ b/src/main/kotlin/apply/config/DatabaseInitializer.kt @@ -432,7 +432,7 @@ class DatabaseInitializer( } private fun populateMailHistories() { - val mailMessage = MailMessage.of( + val mailMessage = MailMessage( subject = "[우아한테크코스] 프리코스를 진행하는 목적과 사전 준비", body = "안녕하세요.", sender = "woowa_course@woowahan.com", diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index 04e48d396..74f57bcdc 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -12,7 +12,7 @@ import javax.persistence.Lob import javax.persistence.OneToMany @Entity -class MailMessage private constructor( +class MailMessage( @Column(nullable = false) val subject: String, @@ -54,43 +54,4 @@ class MailMessage private constructor( private fun hasReservation(): Boolean { return reservations.isNotEmpty() } - - companion object { - fun of( - subject: String, - body: String, - sender: String, - recipients: List, - creatorId: Long - ): MailMessage { - return MailMessage( - subject = subject, - body = body, - sender = sender, - recipients = recipients, - creatorId = creatorId - ) - } - - fun withReservation( - subject: String, - body: String, - sender: String, - recipients: List, - reservationTime: LocalDateTime, - creatorId: Long - ): MailMessage { - return MailMessage( - subject = subject, - body = body, - sender = sender, - recipients = recipients, - creatorId = creatorId - ).apply { - reservations.add( - MailReservation(this, reservationTime = reservationTime) - ) - } - } - } } diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index 04de978b8..767fa02eb 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -32,34 +32,15 @@ fun createMailMessage( id: Long = 0L, creatorId: Long = 0L ): MailMessage { - return MailMessage.of(subject, body, sender, recipients, creatorId) -} - -fun createReservationMailMessage( - subject: String = SUBJECT, - body: String = BODY, - sender: String = SENDER, - recipients: List = RECIPIENTS, - reservationTime: LocalDateTime = RESERVATION_TIME, - id: Long = 0L, - creatorId: Long = 0L -): MailMessage { - return MailMessage.withReservation(subject, body, sender, recipients, reservationTime, creatorId) + return MailMessage(subject, body, sender, recipients, creatorId, id = id) } fun createMailReservation( - subject: String = SUBJECT, - body: String = BODY, - sender: String = SENDER, - recipients: List = RECIPIENTS, + mailMessage: MailMessage = createMailMessage(), reservationTime: LocalDateTime = RESERVATION_TIME, id: Long = 0L, - creatorId: Long = 0L ): MailReservation { - return MailReservation( - createMailMessage(subject, body, sender, recipients, id, creatorId), - reservationTime = reservationTime - ) + return MailReservation(mailMessage, reservationTime = reservationTime, id = id) } fun createSuccessMailHistory( diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index af3513e58..064bee37e 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -1,8 +1,10 @@ package apply.application.mail import apply.createMailData -import apply.createReservationMailMessage +import apply.createMailMessage +import apply.createMailReservation import apply.domain.mail.MailMessageRepository +import apply.domain.mail.MailReservationRepository import apply.domain.mail.getOrThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec @@ -20,7 +22,7 @@ import java.time.LocalDateTime.now class MailMessageIntegrationTest( private val mailMessageService: MailMessageService, private val mailMessageRepository: MailMessageRepository, - private val mailReservationRepository: MailMessageRepository + private val mailReservationRepository: MailReservationRepository ) : BehaviorSpec({ extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) @@ -42,21 +44,23 @@ class MailMessageIntegrationTest( } Given("취소하고 싶은 예약 메일이 있는 경우") { - val mailMessage = mailMessageRepository.save(createReservationMailMessage()) + val mailMessage = mailMessageRepository.save(createMailMessage()) + val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage)) When("메일 예약을 취소하면") { mailMessageService.cancelReservation(mailMessage.id) Then("메일 메시지와 예약이 삭제된다") { mailMessageRepository.findById(mailMessage.id).isEmpty shouldBe true - mailReservationRepository.findAll() shouldBe emptyList() + mailReservationRepository.findById(mailReservation.id).isEmpty shouldNotBe true } } } Given("취소하고 싶은 예약 메일이 처리중인 경우") { - val mailMessage = mailMessageRepository.save(createReservationMailMessage()) - mailMessage.reservation()?.send() + val mailMessage = mailMessageRepository.save(createMailMessage()) + val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage)) + mailReservation.send() When("메일 예약을 취소하면") { Then("에러가 발생하고 메일 메시지와 예약은 남아있다") { @@ -64,16 +68,16 @@ class MailMessageIntegrationTest( mailMessageService.cancelReservation(mailMessage.id) } - val actual = mailMessageRepository.getOrThrow(mailMessage.id) - actual shouldNotBe null - actual.reservation() shouldNotBe null + mailMessageRepository.findById(mailMessage.id).isPresent shouldNotBe true + mailReservationRepository.findById(mailReservation.id).isPresent shouldNotBe true } } } Given("취소하고 싶은 예약 메일이 발송 완료된 경우") { - val mailMessage = mailMessageRepository.save(createReservationMailMessage()) - mailMessage.reservation()?.finish() + val mailMessage = mailMessageRepository.save(createMailMessage()) + val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage)) + mailReservation.finish() When("메일 예약을 취소하면") { Then("에러가 발생하고 메일 메시지와 예약은 남아있다") { @@ -81,9 +85,8 @@ class MailMessageIntegrationTest( mailMessageService.cancelReservation(mailMessage.id) } - val actual = mailMessageRepository.getOrThrow(mailMessage.id) - actual shouldNotBe null - actual.reservation() shouldNotBe null + mailMessageRepository.findById(mailMessage.id).isPresent shouldNotBe true + mailReservationRepository.findById(mailReservation.id).isPresent shouldNotBe true } } } diff --git a/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt index 0b9e1ee3b..11a560c42 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt @@ -1,17 +1,14 @@ package apply.domain.mail import apply.createMailMessage -import apply.createReservationMailMessage import io.kotest.core.spec.style.ExpectSpec import io.kotest.extensions.spring.SpringExtension import io.kotest.matchers.longs.shouldNotBeZero import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager import support.test.RepositoryTest import support.test.spec.afterRootTest -import java.time.LocalDateTime.now @RepositoryTest class MailMessageRepositoryTest( @@ -25,13 +22,6 @@ class MailMessageRepositoryTest( val actual = mailMessageRepository.save(createMailMessage()) actual.id.shouldNotBeZero() } - - expect("메일 메시지와 함께 메일 예약을 저장한다") { - val actual = mailMessageRepository.save(createReservationMailMessage()) - actual.id.shouldNotBeZero() - actual.reservation().shouldNotBeNull() - actual.reservation()!!.id.shouldNotBeZero() - } } context("즉시 발송 메일 메시지 조회") { @@ -44,18 +34,6 @@ class MailMessageRepositoryTest( } } - context("예약 메일 메시지 조회") { - val reservationTime = now().plusHours(3).withMinute(10) - val mailMessage = mailMessageRepository.save(createReservationMailMessage(reservationTime = reservationTime)) - - expect("메일 메시지와 함께 메일 예약을 조회한다") { - val actual = mailMessageRepository.findById(mailMessage.id).get() - actual.shouldNotBeNull() - actual.reservation().shouldNotBeNull() - actual.reservation()!!.reservationTime shouldBe reservationTime - } - } - afterEach { entityManager.flush() entityManager.clear() diff --git a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt index 24c7d6b1b..7946aff40 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt @@ -1,10 +1,8 @@ package apply.domain.mail import apply.createMailMessage -import apply.createReservationMailMessage import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe class MailMessageTest : StringSpec({ "즉시 발송하는 메일 메시지를 생성한다" { @@ -14,12 +12,4 @@ class MailMessageTest : StringSpec({ mailMessage.body shouldBe "내용" mailMessage.reservation() shouldBe null } - - "예약 메일 메시지를 생성한다" { - val mailMessage = createReservationMailMessage(subject = "제목", body = "내용") - - mailMessage.subject shouldBe "제목" - mailMessage.body shouldBe "내용" - mailMessage.reservation() shouldNotBe null - } }) From fb6463593180b4d4d7e293d683b82bc24b493973 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Mon, 9 Oct 2023 14:58:44 +0900 Subject: [PATCH 25/40] refactor: make relationships with MailHistory and MailMessage by id reference --- .../kotlin/apply/application/mail/MailData.kt | 18 +++++++++--------- .../application/mail/MailHistoryService.kt | 11 +++++++---- .../kotlin/apply/config/DatabaseInitializer.kt | 8 ++++---- .../kotlin/apply/domain/mail/MailHistory.kt | 8 ++------ .../kotlin/apply/MailHistoryFixtures.kt.kt | 7 +++---- .../application/MailHistoryServiceTest.kt | 15 ++++++++++----- .../domain/mail/MailHistoryRepositoryTest.kt | 6 +++--- 7 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index 6106cbec9..dc73d590f 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -31,15 +31,6 @@ data class MailData( @field:NotNull var id: Long = 0L ) { - constructor(mailHistory: MailHistory) : this( - mailHistory.mailMessage.subject, - mailHistory.mailMessage.body, - mailHistory.mailMessage.sender, - mailHistory.recipients, - mailHistory.sentTime, - id = mailHistory.id - ) - constructor(mailMessage: MailMessage) : this( mailMessage.subject, mailMessage.body, @@ -48,6 +39,15 @@ data class MailData( id = mailMessage.id ) + constructor(mailMessage: MailMessage, mailHistory: MailHistory) : this( + mailMessage.subject, + mailMessage.body, + mailMessage.sender, + mailHistory.recipients, + mailHistory.sentTime, + id = mailHistory.id + ) + fun toMailMessage(): MailMessage { // TODO: 작성자 ID 바인딩 return MailMessage(subject, body, sender, recipients, 1L) diff --git a/src/main/kotlin/apply/application/mail/MailHistoryService.kt b/src/main/kotlin/apply/application/mail/MailHistoryService.kt index fa73669ee..03fda6b61 100644 --- a/src/main/kotlin/apply/application/mail/MailHistoryService.kt +++ b/src/main/kotlin/apply/application/mail/MailHistoryService.kt @@ -23,21 +23,24 @@ class MailHistoryService( val mailHistories = mutableListOf() if (event.succeedRecipients.isNotEmpty()) { - mailHistories.add(MailHistory(mailMessage, event.succeedRecipients, true)) + mailHistories.add(MailHistory(mailMessage.id, event.succeedRecipients, true)) } if (event.failedRecipients.isNotEmpty()) { - mailHistories.add(MailHistory(mailMessage, event.failedRecipients, false)) + mailHistories.add(MailHistory(mailMessage.id, event.failedRecipients, false)) } mailHistoryRepository.saveAll(mailHistories) } fun findAll(): List { - return mailHistoryRepository.findAll().map { MailData(it) } + val histories = mailHistoryRepository.findAll() + val messagesById = mailMessageRepository.findAllById(histories.map { it.mailMessageId }).associateBy { it.id } + return histories.map { MailData(messagesById.getValue(it.mailMessageId), it) } } fun getById(mailHistoryId: Long): MailData { val mailHistory = mailHistoryRepository.getOrThrow(mailHistoryId) - return MailData(mailHistory) + val mailMessage = mailMessageRepository.getOrThrow(mailHistory.mailMessageId) + return MailData(mailMessage, mailHistory) } } diff --git a/src/main/kotlin/apply/config/DatabaseInitializer.kt b/src/main/kotlin/apply/config/DatabaseInitializer.kt index 869190c8d..4f9981da9 100644 --- a/src/main/kotlin/apply/config/DatabaseInitializer.kt +++ b/src/main/kotlin/apply/config/DatabaseInitializer.kt @@ -443,14 +443,14 @@ class DatabaseInitializer( val mailHistories = listOf( MailHistory( - mailMessage = mailMessage, + mailMessageId = mailMessage.id, recipients = mailMessage.recipients.subList(0, 2), - true + success = true ), MailHistory( - mailMessage = mailMessage, + mailMessageId = mailMessage.id, recipients = mailMessage.recipients.subList(3, 4), - true + success = true ) ) mailHistoryRepository.saveAll(mailHistories) diff --git a/src/main/kotlin/apply/domain/mail/MailHistory.kt b/src/main/kotlin/apply/domain/mail/MailHistory.kt index 7caf2a9c5..517a64db2 100644 --- a/src/main/kotlin/apply/domain/mail/MailHistory.kt +++ b/src/main/kotlin/apply/domain/mail/MailHistory.kt @@ -6,16 +6,12 @@ import java.time.LocalDateTime import javax.persistence.Column import javax.persistence.Convert import javax.persistence.Entity -import javax.persistence.ForeignKey -import javax.persistence.JoinColumn import javax.persistence.Lob -import javax.persistence.ManyToOne @Entity class MailHistory( - @ManyToOne - @JoinColumn(nullable = false, foreignKey = ForeignKey(name = "fk_mail_history_to_mail_message")) - val mailMessage: MailMessage, + @Column(nullable = false) + val mailMessageId: Long = 0L, @Column(nullable = false) @Convert(converter = StringToListConverter::class) diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index 767fa02eb..8ba2e7da1 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -9,6 +9,7 @@ import java.time.LocalDateTime private const val SUBJECT: String = "메일제목" private const val BODY: String = "메일 본문 입니다." private const val SENDER: String = "woowacourse@email.com" +private const val MAIL_MESSAGE_ID: Long = 1L private val RECIPIENTS: List = listOf("test1@email.com", "test2@email.com") private val SENT_TIME: LocalDateTime = LocalDateTime.now() private val RESERVATION_TIME: LocalDateTime = LocalDateTime.now().plusHours(3).withMinute(0) @@ -44,10 +45,8 @@ fun createMailReservation( } fun createSuccessMailHistory( - subject: String = SUBJECT, - body: String = BODY, - sender: String = SENDER, + mailMessageId: Long = MAIL_MESSAGE_ID, recipients: List = RECIPIENTS ): MailHistory { - return MailHistory(createMailMessage(subject, body, sender, recipients), recipients, true) + return MailHistory(mailMessageId, recipients, true) } diff --git a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt index ee3f818bd..6439e5a33 100644 --- a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt +++ b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt @@ -1,11 +1,12 @@ package apply.application import apply.application.mail.MailHistoryService +import apply.createMailMessage import apply.createSuccessMailHistory import apply.domain.mail.MailHistoryRepository import apply.domain.mail.MailMessageRepository import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.shouldBe +import io.kotest.matchers.collections.shouldContain import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk @@ -18,17 +19,21 @@ class MailHistoryServiceTest : BehaviorSpec({ val mailHistoryService = MailHistoryService(mailHistoryRepository, mailMessageRepository) Given("메일 이력이 있는 경우") { + val mailMessage = createMailMessage(id = 1L) + every { mailHistoryRepository.findAll() } returns listOf( - createSuccessMailHistory(subject = "제목1"), - createSuccessMailHistory(subject = "제목2") + createSuccessMailHistory(mailMessage.id, listOf("a@a.com")), + createSuccessMailHistory(mailMessage.id, listOf("b@b.com")) ) + every { mailMessageRepository.findAllById(any()) } returns listOf(mailMessage) + When("모든 메일 이력을 조회하면") { val actual = mailHistoryService.findAll() Then("모든 메일 이력을 확인할 수 있다") { - actual[0].subject shouldBe "제목1" - actual[1].subject shouldBe "제목2" + actual[0].recipients shouldContain "a@a.com" + actual[1].recipients shouldContain "b@b.com" } } } diff --git a/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt index 896f0079a..c753ceaae 100644 --- a/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailHistoryRepositoryTest.kt @@ -21,19 +21,19 @@ class MailHistoryRepositoryTest( val mailMessage = mailMessageRepository.save(createMailMessage()) expect("메일 발송 성공에 대한 히스토리를 저장한다") { - val actual = mailHistoryRepository.save(MailHistory(mailMessage, mailMessage.recipients, true)) + val actual = mailHistoryRepository.save(MailHistory(mailMessage.id, mailMessage.recipients, true)) actual.id.shouldNotBeZero() } } context("메일 히스토리 조회") { val mailMessage = mailMessageRepository.save(createMailMessage()) - mailHistoryRepository.save(MailHistory(mailMessage, mailMessage.recipients, true)) + mailHistoryRepository.save(MailHistory(mailMessage.id, mailMessage.recipients, true)) expect("메일 발송 성공에 대한 히스토리를 저장한다") { val actual = mailHistoryRepository.findAll() actual.shouldNotBeEmpty() - actual[0].mailMessage.id.shouldNotBeZero() + actual[0].mailMessageId.shouldNotBeZero() } } From 237ba8c4b85fda3c260f3155023b3236a686b25d Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 08:05:15 +0900 Subject: [PATCH 26/40] refactor: make relationships with MailReservation and MailMessage by id reference --- .../kotlin/apply/application/mail/MailDtos.kt | 34 ++++++------------- .../application/mail/MailMessageService.kt | 13 +++---- .../mail/MailReservationService.kt | 7 +++- .../kotlin/apply/domain/mail/MailMessage.kt | 25 +------------- .../apply/domain/mail/MailReservation.kt | 11 +----- .../domain/mail/MailReservationRepository.kt | 2 ++ .../kotlin/apply/MailHistoryFixtures.kt.kt | 4 +-- .../mail/MailMessageIntegrationTest.kt | 28 +++++++-------- .../domain/mail/MailMessageRepositoryTest.kt | 2 -- .../apply/domain/mail/MailMessageTest.kt | 1 - 10 files changed, 44 insertions(+), 83 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailDtos.kt b/src/main/kotlin/apply/application/mail/MailDtos.kt index 82b93a4bb..9ccbb6917 100644 --- a/src/main/kotlin/apply/application/mail/MailDtos.kt +++ b/src/main/kotlin/apply/application/mail/MailDtos.kt @@ -6,47 +6,35 @@ import apply.domain.mail.MailReservationStatus import java.time.LocalDateTime data class MailMessageResponse( + val id: Long, val subject: String, val body: String, val sender: String, val recipients: List, val createdDateTime: LocalDateTime, - val reservation: MailReservationSimpleResponse?, - val id: Long + val reservation: MailReservationResponse? ) { - constructor(mailMessage: MailMessage) : this( + constructor(mailMessage: MailMessage, mailReservation: MailReservation? = null) : this( + mailMessage.id, mailMessage.subject, mailMessage.body, mailMessage.sender, mailMessage.recipients, mailMessage.createdDateTime, - mailMessage.reservation()?.let { MailReservationSimpleResponse(it) }, - mailMessage.id - ) -} - -data class MailReservationSimpleResponse( - val status: MailReservationStatus, - val reservationTime: LocalDateTime, - val id: Long, -) { - constructor(mailReservation: MailReservation) : this( - mailReservation.status, - mailReservation.reservationTime, - mailReservation.id + mailReservation?.let { MailReservationResponse(it) } ) } data class MailReservationResponse( - val mailMessage: MailMessageResponse, - val status: MailReservationStatus, - val reservationTime: LocalDateTime, val id: Long, + val mailMessageId: Long, + val status: MailReservationStatus, + val reservationTime: LocalDateTime ) { constructor(mailReservation: MailReservation) : this( - MailMessageResponse(mailReservation.mailMessage), + mailReservation.id, + mailReservation.mailMessageId, mailReservation.status, - mailReservation.reservationTime, - mailReservation.id + mailReservation.reservationTime ) } diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 9f0946b8a..c27796d01 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -3,7 +3,6 @@ package apply.application.mail import apply.domain.mail.MailMessageRepository import apply.domain.mail.MailReservation import apply.domain.mail.MailReservationRepository -import apply.domain.mail.getOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -16,14 +15,16 @@ class MailMessageService( fun reserve(request: MailData): MailMessageResponse { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val mailReservation = mailReservationRepository.save( - MailReservation(mailMessage, reservationTime = request.sentTime) + MailReservation(mailMessageId = mailMessage.id, reservationTime = request.sentTime) ) - return MailMessageResponse(mailMessage) + return MailMessageResponse(mailMessage, mailReservation) } fun cancelReservation(mailMessageId: Long) { - val mailMessage = mailMessageRepository.getOrThrow(mailMessageId) - check(mailMessage.canDelete()) { "예약 취소할 수 없는 메일입니다." } - mailMessageRepository.deleteById(mailMessageId) + val mailReservation = mailReservationRepository.findByMailMessageId(mailMessageId) + ?: throw IllegalArgumentException("메일 예약이 존재하지 않습니다. email: $mailMessageId") + check(mailReservation.canCancel()) { "예약 취소할 수 없는 메일입니다." } + mailReservationRepository.deleteById(mailReservation.id) + mailMessageRepository.deleteById(mailReservation.mailMessageId) } } diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index 48d763f76..c7b5a5b12 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -1,5 +1,6 @@ package apply.application.mail +import apply.domain.mail.MailMessageRepository import apply.domain.mail.MailReservationRepository import apply.domain.mail.MailReservationStatus import org.springframework.stereotype.Service @@ -11,6 +12,7 @@ import java.time.LocalDateTime class MailReservationService( private val mailService: MailService, private val mailReservationRepository: MailReservationRepository, + private val mailMessageRepository: MailMessageRepository ) { fun findByWaitingStatus(): List { return mailReservationRepository.findByStatus(MailReservationStatus.WAITING) @@ -23,10 +25,13 @@ class MailReservationService( standardTime.plusMinutes(1), MailReservationStatus.WAITING ) + val messagesById = mailMessageRepository + .findAllById(reservations.map { it.mailMessageId }) + .associateBy { it.id } reservations.forEach { mailReservation -> mailReservation.send() - mailService.sendMailsByBcc(MailData(mailReservation.mailMessage), emptyMap()) { + mailService.sendMailsByBcc(MailData(messagesById.getValue(mailReservation.id)), emptyMap()) { mailReservation.finish() } } diff --git a/src/main/kotlin/apply/domain/mail/MailMessage.kt b/src/main/kotlin/apply/domain/mail/MailMessage.kt index 74f57bcdc..d1d83af4e 100644 --- a/src/main/kotlin/apply/domain/mail/MailMessage.kt +++ b/src/main/kotlin/apply/domain/mail/MailMessage.kt @@ -3,13 +3,10 @@ package apply.domain.mail import support.domain.BaseEntity import support.domain.StringToListConverter import java.time.LocalDateTime -import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Convert import javax.persistence.Entity -import javax.persistence.FetchType import javax.persistence.Lob -import javax.persistence.OneToMany @Entity class MailMessage( @@ -34,24 +31,4 @@ class MailMessage( @Column(nullable = false) val createdDateTime: LocalDateTime = LocalDateTime.now(), id: Long = 0L -) : BaseEntity(id) { - - @OneToMany(mappedBy = "mailMessage", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) - var reservations: MutableList = mutableListOf() - - fun reservation(): MailReservation? { - if (!hasReservation()) { - return null - } - - return reservations.first() - } - - fun canDelete(): Boolean { - return this.reservation()?.canCancel() ?: true - } - - private fun hasReservation(): Boolean { - return reservations.isNotEmpty() - } -} +) : BaseEntity(id) diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index a59d319f5..053c73ef3 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -6,21 +6,12 @@ import javax.persistence.Column import javax.persistence.Entity import javax.persistence.EnumType import javax.persistence.Enumerated -import javax.persistence.ForeignKey -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne private const val PERIOD_MINUTES: Long = 10L @Entity class MailReservation( - - @ManyToOne - @JoinColumn( - name = "mail_message_id", nullable = false, - foreignKey = ForeignKey(name = "fk_mail_reservation_to_mail_message") - ) - val mailMessage: MailMessage, + val mailMessageId: Long = 0L, @Column(nullable = false) @Enumerated(EnumType.STRING) diff --git a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt index b7f53b8cd..a27ca1aa1 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt @@ -14,4 +14,6 @@ interface MailReservationRepository : JpaRepository { to: LocalDateTime, status: MailReservationStatus ): List + + fun findByMailMessageId(mailMessageId: Long): MailReservation? } diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index 8ba2e7da1..3e583da11 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -37,11 +37,11 @@ fun createMailMessage( } fun createMailReservation( - mailMessage: MailMessage = createMailMessage(), + mailMessageId: Long = MAIL_MESSAGE_ID, reservationTime: LocalDateTime = RESERVATION_TIME, id: Long = 0L, ): MailReservation { - return MailReservation(mailMessage, reservationTime = reservationTime, id = id) + return MailReservation(mailMessageId, reservationTime = reservationTime, id = id) } fun createSuccessMailHistory( diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index 064bee37e..eb1111a7f 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -10,9 +10,9 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.extensions.spring.SpringTestExtension import io.kotest.extensions.spring.SpringTestLifecycleMode -import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import org.springframework.data.repository.findByIdOrNull import org.springframework.transaction.annotation.Transactional import support.test.IntegrationTest import java.time.LocalDateTime.now @@ -34,32 +34,32 @@ class MailMessageIntegrationTest( val mailMessage = mailMessageService.reserve(mailData) Then("메일 메시지와 예약이 생성된다") { - val actual = mailMessageRepository.getOrThrow(mailMessage.id) + mailMessageRepository.getOrThrow(mailMessage.id).subject shouldBe mailData.subject - actual.subject shouldBe mailData.subject - actual.reservation().shouldNotBeNull() - actual.reservation()!!.reservationTime shouldBe reservationTime + val reservation = mailReservationRepository.findByMailMessageId(mailMessage.id) + reservation shouldNotBe null + reservation?.reservationTime shouldBe reservationTime } } } Given("취소하고 싶은 예약 메일이 있는 경우") { val mailMessage = mailMessageRepository.save(createMailMessage()) - val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage)) + val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage.id)) When("메일 예약을 취소하면") { mailMessageService.cancelReservation(mailMessage.id) Then("메일 메시지와 예약이 삭제된다") { - mailMessageRepository.findById(mailMessage.id).isEmpty shouldBe true - mailReservationRepository.findById(mailReservation.id).isEmpty shouldNotBe true + mailMessageRepository.findByIdOrNull(mailMessage.id) shouldBe null + mailReservationRepository.findByIdOrNull(mailReservation.id) shouldBe null } } } Given("취소하고 싶은 예약 메일이 처리중인 경우") { val mailMessage = mailMessageRepository.save(createMailMessage()) - val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage)) + val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage.id)) mailReservation.send() When("메일 예약을 취소하면") { @@ -68,15 +68,15 @@ class MailMessageIntegrationTest( mailMessageService.cancelReservation(mailMessage.id) } - mailMessageRepository.findById(mailMessage.id).isPresent shouldNotBe true - mailReservationRepository.findById(mailReservation.id).isPresent shouldNotBe true + mailMessageRepository.findByIdOrNull(mailMessage.id) shouldNotBe null + mailReservationRepository.findByIdOrNull(mailReservation.id) shouldNotBe null } } } Given("취소하고 싶은 예약 메일이 발송 완료된 경우") { val mailMessage = mailMessageRepository.save(createMailMessage()) - val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage)) + val mailReservation = mailReservationRepository.save(createMailReservation(mailMessage.id)) mailReservation.finish() When("메일 예약을 취소하면") { @@ -85,8 +85,8 @@ class MailMessageIntegrationTest( mailMessageService.cancelReservation(mailMessage.id) } - mailMessageRepository.findById(mailMessage.id).isPresent shouldNotBe true - mailReservationRepository.findById(mailReservation.id).isPresent shouldNotBe true + mailMessageRepository.findByIdOrNull(mailMessage.id) shouldNotBe null + mailReservationRepository.findByIdOrNull(mailReservation.id) shouldNotBe null } } } diff --git a/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt index 11a560c42..23e2231ca 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageRepositoryTest.kt @@ -4,7 +4,6 @@ import apply.createMailMessage import io.kotest.core.spec.style.ExpectSpec import io.kotest.extensions.spring.SpringExtension import io.kotest.matchers.longs.shouldNotBeZero -import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager import support.test.RepositoryTest @@ -30,7 +29,6 @@ class MailMessageRepositoryTest( expect("메일 메시지만 조회한다") { val actual = mailMessageRepository.findById(mailMessage.id).get() actual.shouldNotBeNull() - actual.reservation().shouldBeNull() } } diff --git a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt index 7946aff40..da8a570a8 100644 --- a/src/test/kotlin/apply/domain/mail/MailMessageTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailMessageTest.kt @@ -10,6 +10,5 @@ class MailMessageTest : StringSpec({ mailMessage.subject shouldBe "제목" mailMessage.body shouldBe "내용" - mailMessage.reservation() shouldBe null } }) From 4bf23cb067451d4331dc7f3e36d2e16f21eaabb7 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 08:48:51 +0900 Subject: [PATCH 27/40] refactor: add synchronous function to send mail --- .../mail/MailReservationService.kt | 6 +-- .../apply/application/mail/MailService.kt | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt index c7b5a5b12..e7619318f 100644 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ b/src/main/kotlin/apply/application/mail/MailReservationService.kt @@ -31,9 +31,9 @@ class MailReservationService( reservations.forEach { mailReservation -> mailReservation.send() - mailService.sendMailsByBcc(MailData(messagesById.getValue(mailReservation.id)), emptyMap()) { - mailReservation.finish() - } + mailReservationRepository.save(mailReservation) + mailService.sendMailsByBccSynchronous(MailData(messagesById.getValue(mailReservation.id)), emptyMap()) + mailReservation.finish() } } } diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/MailService.kt index d4daaeece..1525e1180 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/MailService.kt @@ -2,6 +2,9 @@ package apply.application.mail import apply.application.ApplicationProperties import apply.domain.applicationform.ApplicationFormSubmittedEvent +import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.MailMessageRepository import apply.domain.mail.MailSentEvent import apply.domain.recruitment.RecruitmentRepository import apply.domain.recruitment.getOrThrow @@ -24,6 +27,8 @@ private const val MAIL_SENDING_UNIT: Int = 50 class MailService( private val userRepository: UserRepository, private val recruitmentRepository: RecruitmentRepository, + private val mailMessageRepository: MailMessageRepository, + private val mailHistoryRepository: MailHistoryRepository, private val applicationProperties: ApplicationProperties, private val templateEngine: ISpringTemplateEngine, private val mailSender: MailSender, @@ -89,7 +94,7 @@ class MailService( } @Async - fun sendMailsByBcc(request: MailData, files: Map, afterAction: () -> Unit = {}) { + fun sendMailsByBcc(request: MailData, files: Map) { val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username @@ -102,7 +107,22 @@ class MailService( } eventPublisher.publishEvent(MailSentEvent(request, succeeded, failed)) - afterAction() + } + + fun sendMailsByBccSynchronous(request: MailData, files: Map) { + val mailMessage = mailMessageRepository.save(request.toMailMessage()) + val body = generateMailBody(request) + val recipients = mailMessage.recipients + mailProperties.username + + val succeeded = mutableListOf() + val failed = mutableListOf() + for (addresses in recipients.chunked(MAIL_SENDING_UNIT)) { + runCatching { mailSender.sendBcc(addresses, mailMessage.subject, body, files) } + .onSuccess { succeeded.addAll(addresses) } + .onFailure { failed.addAll(addresses) } + } + + saveMailHistories(mailMessage.id, succeeded, failed) } fun generateMailBody(mailData: MailData): String { @@ -116,4 +136,22 @@ class MailService( } return templateEngine.process("mail/common", context) } + + private fun saveMailHistories( + mailMessageId: Long, + succeeded: MutableList, + failed: MutableList + ) { + val mailHistories = mutableListOf() + + if (succeeded.isNotEmpty()) { + mailHistories.add(MailHistory(mailMessageId, succeeded, true)) + } + + if (failed.isNotEmpty()) { + mailHistories.add(MailHistory(mailMessageId, failed, false)) + } + + mailHistoryRepository.saveAll(mailHistories) + } } From 19cee5863bcadd5fda7085ba6c897578b07c430e Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 08:51:55 +0900 Subject: [PATCH 28/40] refactor: delete MailSentEvent and Listener --- .../application/mail/MailHistoryService.kt | 20 ------------------- .../apply/application/mail/MailService.kt | 4 ++-- .../kotlin/apply/domain/mail/MailSentEvent.kt | 9 --------- 3 files changed, 2 insertions(+), 31 deletions(-) delete mode 100644 src/main/kotlin/apply/domain/mail/MailSentEvent.kt diff --git a/src/main/kotlin/apply/application/mail/MailHistoryService.kt b/src/main/kotlin/apply/application/mail/MailHistoryService.kt index 03fda6b61..fe89814f7 100644 --- a/src/main/kotlin/apply/application/mail/MailHistoryService.kt +++ b/src/main/kotlin/apply/application/mail/MailHistoryService.kt @@ -1,11 +1,8 @@ package apply.application.mail -import apply.domain.mail.MailHistory import apply.domain.mail.MailHistoryRepository import apply.domain.mail.MailMessageRepository -import apply.domain.mail.MailSentEvent import apply.domain.mail.getOrThrow -import org.springframework.context.event.EventListener import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,23 +12,6 @@ class MailHistoryService( private val mailHistoryRepository: MailHistoryRepository, private val mailMessageRepository: MailMessageRepository ) { - @EventListener - fun onMailSentEvent(event: MailSentEvent) { - val mailData = event.mailData - val mailMessage = mailMessageRepository.findById(mailData.id) - .let { mailMessageRepository.save(mailData.toMailMessage()) } - - val mailHistories = mutableListOf() - if (event.succeedRecipients.isNotEmpty()) { - mailHistories.add(MailHistory(mailMessage.id, event.succeedRecipients, true)) - } - if (event.failedRecipients.isNotEmpty()) { - mailHistories.add(MailHistory(mailMessage.id, event.failedRecipients, false)) - } - - mailHistoryRepository.saveAll(mailHistories) - } - fun findAll(): List { val histories = mailHistoryRepository.findAll() val messagesById = mailMessageRepository.findAllById(histories.map { it.mailMessageId }).associateBy { it.id } diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/MailService.kt index 1525e1180..dbcbe793c 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/MailService.kt @@ -5,7 +5,6 @@ import apply.domain.applicationform.ApplicationFormSubmittedEvent import apply.domain.mail.MailHistory import apply.domain.mail.MailHistoryRepository import apply.domain.mail.MailMessageRepository -import apply.domain.mail.MailSentEvent import apply.domain.recruitment.RecruitmentRepository import apply.domain.recruitment.getOrThrow import apply.domain.user.PasswordResetEvent @@ -95,6 +94,7 @@ class MailService( @Async fun sendMailsByBcc(request: MailData, files: Map) { + val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username @@ -106,7 +106,7 @@ class MailService( .onFailure { failed.addAll(addresses) } } - eventPublisher.publishEvent(MailSentEvent(request, succeeded, failed)) + saveMailHistories(mailMessage.id, succeeded, failed) } fun sendMailsByBccSynchronous(request: MailData, files: Map) { diff --git a/src/main/kotlin/apply/domain/mail/MailSentEvent.kt b/src/main/kotlin/apply/domain/mail/MailSentEvent.kt deleted file mode 100644 index d117573ab..000000000 --- a/src/main/kotlin/apply/domain/mail/MailSentEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package apply.domain.mail - -import apply.application.mail.MailData - -data class MailSentEvent( - val mailData: MailData, - val succeedRecipients: List, - val failedRecipients: List -) From 8bf6500571740802a7d3c2afe1627614467b2090 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 10:11:26 +0900 Subject: [PATCH 29/40] test: add test for sending reservation mail --- .../mail/MailReservationIntegrationTest.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt diff --git a/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt new file mode 100644 index 000000000..c0df564e8 --- /dev/null +++ b/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt @@ -0,0 +1,45 @@ +package apply.application.mail + +import apply.config.TestMailConfiguration +import apply.createMailMessage +import apply.createMailReservation +import apply.domain.mail.MailMessageRepository +import apply.domain.mail.MailReservationRepository +import apply.domain.mail.MailReservationStatus +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe +import org.springframework.context.annotation.Import +import org.springframework.transaction.annotation.Transactional +import support.test.IntegrationTest +import java.time.LocalDateTime.now + +@Import(TestMailConfiguration::class) +@Transactional +@IntegrationTest +class MailReservationIntegrationTest( + private val mailReservationService: MailReservationService, + private val mailReservationRepository: MailReservationRepository, + private val mailMessageRepository: MailMessageRepository +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("특정 시간에 발송할 예약 메일이 있는 경우") { + val reservationTime = now().plusHours(3).withMinute(10) + val mailMessage1 = mailMessageRepository.save(createMailMessage()) + val mailMessage2 = mailMessageRepository.save(createMailMessage()) + + mailReservationRepository.save(createMailReservation(mailMessage1.id, reservationTime)) + mailReservationRepository.save(createMailReservation(mailMessage2.id, reservationTime.plusHours(3))) + + When("해당 시간에 메일 발송 요청을 하면") { + mailReservationService.sendMail(reservationTime) + + Then("메일 전송이 완료된다") { + val actual = mailReservationRepository.findByStatus(MailReservationStatus.FINISHED) + actual.size shouldBe 1 + } + } + } +}) From f2760b41654f395e9fb71263a73745241b8c4e57 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 10:35:59 +0900 Subject: [PATCH 30/40] refactor: extract functions to get reservation time --- .../kotlin/apply/domain/mail/MailReservation.kt | 6 +++--- src/main/kotlin/support/Dates.kt | 5 +++++ src/test/kotlin/apply/MailHistoryFixtures.kt.kt | 8 +++++++- .../mail/MailMessageIntegrationTest.kt | 4 ++-- .../mail/MailReservationIntegrationTest.kt | 4 ++-- .../apply/domain/mail/MailReservationTest.kt | 15 ++++++++------- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/apply/domain/mail/MailReservation.kt b/src/main/kotlin/apply/domain/mail/MailReservation.kt index 053c73ef3..d685eef88 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservation.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservation.kt @@ -7,7 +7,7 @@ import javax.persistence.Entity import javax.persistence.EnumType import javax.persistence.Enumerated -private const val PERIOD_MINUTES: Long = 10L +const val MAIL_RESERVATION_PERIOD_MINUTES: Long = 15 @Entity class MailReservation( @@ -44,8 +44,8 @@ class MailReservation( } // TODO: validator 추출 - require(reservationTime.minute % PERIOD_MINUTES == 0L) { - "예약 메일의 예약시간은 10분 단위로 설정해야 합니다." + require(reservationTime.minute % MAIL_RESERVATION_PERIOD_MINUTES == 0L) { + "예약 메일의 예약시간은 ${MAIL_RESERVATION_PERIOD_MINUTES}분 단위로 설정해야 합니다." } } } diff --git a/src/main/kotlin/support/Dates.kt b/src/main/kotlin/support/Dates.kt index 517beeed2..ecc193a46 100644 --- a/src/main/kotlin/support/Dates.kt +++ b/src/main/kotlin/support/Dates.kt @@ -2,6 +2,7 @@ package support import java.time.LocalDate import java.time.LocalDateTime +import java.time.temporal.ChronoUnit fun createLocalDateTime( year: Int, @@ -22,3 +23,7 @@ fun createLocalDate( ): LocalDate { return LocalDate.of(year, month, dayOfMonth) } + +fun LocalDateTime.nextMinutes(standard: Long): LocalDateTime { + return this.truncatedTo(ChronoUnit.MINUTES).plusMinutes((standard - (this.minute % standard))) +} diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt index 3e583da11..b59b34182 100644 --- a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -1,9 +1,11 @@ package apply import apply.application.mail.MailData +import apply.domain.mail.MAIL_RESERVATION_PERIOD_MINUTES import apply.domain.mail.MailHistory import apply.domain.mail.MailMessage import apply.domain.mail.MailReservation +import support.nextMinutes import java.time.LocalDateTime private const val SUBJECT: String = "메일제목" @@ -12,7 +14,7 @@ private const val SENDER: String = "woowacourse@email.com" private const val MAIL_MESSAGE_ID: Long = 1L private val RECIPIENTS: List = listOf("test1@email.com", "test2@email.com") private val SENT_TIME: LocalDateTime = LocalDateTime.now() -private val RESERVATION_TIME: LocalDateTime = LocalDateTime.now().plusHours(3).withMinute(0) +private val RESERVATION_TIME: LocalDateTime = createAvailableReservationTime() fun createMailData( subject: String = SUBJECT, @@ -50,3 +52,7 @@ fun createSuccessMailHistory( ): MailHistory { return MailHistory(mailMessageId, recipients, true) } + +fun createAvailableReservationTime(): LocalDateTime { + return LocalDateTime.now().nextMinutes(MAIL_RESERVATION_PERIOD_MINUTES).plusHours(3) +} diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index eb1111a7f..cc6e07af7 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -1,5 +1,6 @@ package apply.application.mail +import apply.createAvailableReservationTime import apply.createMailData import apply.createMailMessage import apply.createMailReservation @@ -15,7 +16,6 @@ import io.kotest.matchers.shouldNotBe import org.springframework.data.repository.findByIdOrNull import org.springframework.transaction.annotation.Transactional import support.test.IntegrationTest -import java.time.LocalDateTime.now @Transactional @IntegrationTest @@ -27,7 +27,7 @@ class MailMessageIntegrationTest( extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) Given("예약하고 싶은 메일 내용이 있는 경우") { - val reservationTime = now().plusHours(3).withMinute(10) + val reservationTime = createAvailableReservationTime() val mailData = createMailData(sentTime = reservationTime) When("메일을 예약하면") { diff --git a/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt index c0df564e8..e4fbb0bc0 100644 --- a/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt @@ -1,6 +1,7 @@ package apply.application.mail import apply.config.TestMailConfiguration +import apply.createAvailableReservationTime import apply.createMailMessage import apply.createMailReservation import apply.domain.mail.MailMessageRepository @@ -13,7 +14,6 @@ import io.kotest.matchers.shouldBe import org.springframework.context.annotation.Import import org.springframework.transaction.annotation.Transactional import support.test.IntegrationTest -import java.time.LocalDateTime.now @Import(TestMailConfiguration::class) @Transactional @@ -26,7 +26,7 @@ class MailReservationIntegrationTest( extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) Given("특정 시간에 발송할 예약 메일이 있는 경우") { - val reservationTime = now().plusHours(3).withMinute(10) + val reservationTime = createAvailableReservationTime() val mailMessage1 = mailMessageRepository.save(createMailMessage()) val mailMessage2 = mailMessageRepository.save(createMailMessage()) diff --git a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt index 8b409642c..5d9424ef4 100644 --- a/src/test/kotlin/apply/domain/mail/MailReservationTest.kt +++ b/src/test/kotlin/apply/domain/mail/MailReservationTest.kt @@ -1,12 +1,12 @@ package apply.domain.mail +import apply.createAvailableReservationTime import apply.createMailReservation import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.inspectors.forAll import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe -import java.time.LocalDateTime.now class MailReservationTest : StringSpec({ "메일 예약을 생성한다" { @@ -16,15 +16,16 @@ class MailReservationTest : StringSpec({ mailReservation.status shouldBe MailReservationStatus.WAITING } - "예약 메일의 예약 시간은 10분 단위로 설정할 수 있다" { - val future = now().plusHours(1) - val mailReservation = createMailReservation(reservationTime = future.withMinute(30)) + "예약 메일의 예약 시간은 15분 단위로 설정할 수 있다" { + val future = createAvailableReservationTime() + val mailReservation = createMailReservation(reservationTime = future) mailReservation shouldNotBe null + mailReservation.reservationTime shouldBe future } - "예약 메일의 예약 시간이 10분 단위가 아닐 경우 예약이 불가능하다" { - val future = now().plusHours(1) + "예약 메일의 예약 시간이 15분 단위가 아닐 경우 예약이 불가능하다" { + val future = createAvailableReservationTime() listOf(future.withMinute(7), future.withMinute(23)).forAll { reservationTime -> shouldThrow { @@ -34,7 +35,7 @@ class MailReservationTest : StringSpec({ } "예약 메일의 예약 시간이 과거라면 예약이 불가능하다" { - val past = now().minusHours(2) + val past = createAvailableReservationTime().minusDays(1) shouldThrow { createMailReservation(reservationTime = past.withMinute(0)) } From 49f91f6a19668ea3c34f0886ad27c9b8aa0a96aa Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 11:49:52 +0900 Subject: [PATCH 31/40] refactor: move mails functions into MailMessageService --- .../kotlin/apply/application/mail/MailDtos.kt | 31 +++++++++-- .../application/mail/MailMessageService.kt | 53 +++++++++++++++++++ .../mail/MailReservationService.kt | 39 -------------- .../ui/api/MailReservationRestController.kt | 23 -------- .../kotlin/apply/ui/api/MailRestController.kt | 12 ++++- .../mail/MailMessageIntegrationTest.kt | 19 +++++++ .../mail/MailReservationIntegrationTest.kt | 45 ---------------- 7 files changed, 111 insertions(+), 111 deletions(-) delete mode 100644 src/main/kotlin/apply/application/mail/MailReservationService.kt delete mode 100644 src/main/kotlin/apply/ui/api/MailReservationRestController.kt delete mode 100644 src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt diff --git a/src/main/kotlin/apply/application/mail/MailDtos.kt b/src/main/kotlin/apply/application/mail/MailDtos.kt index 9ccbb6917..b09bc05d8 100644 --- a/src/main/kotlin/apply/application/mail/MailDtos.kt +++ b/src/main/kotlin/apply/application/mail/MailDtos.kt @@ -1,5 +1,6 @@ package apply.application.mail +import apply.domain.mail.MailHistory import apply.domain.mail.MailMessage import apply.domain.mail.MailReservation import apply.domain.mail.MailReservationStatus @@ -12,16 +13,24 @@ data class MailMessageResponse( val sender: String, val recipients: List, val createdDateTime: LocalDateTime, - val reservation: MailReservationResponse? + val sentTime: LocalDateTime?, + val reservation: MailReservationResponse?, + val histories: List ) { - constructor(mailMessage: MailMessage, mailReservation: MailReservation? = null) : this( + constructor( + mailMessage: MailMessage, + mailReservation: MailReservation? = null, + mailHistories: List = emptyList() + ) : this( mailMessage.id, mailMessage.subject, mailMessage.body, mailMessage.sender, mailMessage.recipients, mailMessage.createdDateTime, - mailReservation?.let { MailReservationResponse(it) } + mailHistories.firstOrNull()?.sentTime, + mailReservation?.let { MailReservationResponse(it) }, + mailHistories.map { MailHistoryResponse(it) } ) } @@ -38,3 +47,19 @@ data class MailReservationResponse( mailReservation.reservationTime ) } + +data class MailHistoryResponse( + val id: Long, + val mailMessageId: Long, + val recipients: List, + val success: Boolean, + val sentTime: LocalDateTime +) { + constructor(mailHistory: MailHistory) : this( + mailHistory.id, + mailHistory.mailMessageId, + mailHistory.recipients, + mailHistory.success, + mailHistory.sentTime + ) +} diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index c27796d01..b7a8a3856 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -1,17 +1,48 @@ package apply.application.mail +import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.MailMessage import apply.domain.mail.MailMessageRepository import apply.domain.mail.MailReservation import apply.domain.mail.MailReservationRepository +import apply.domain.mail.MailReservationStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime @Transactional @Service class MailMessageService( + private val mailService: MailService, private val mailMessageRepository: MailMessageRepository, private val mailReservationRepository: MailReservationRepository, + private val mailHistoryRepository: MailHistoryRepository ) { + fun findSentMails(): List { + val histories = mailHistoryRepository.findAll() + val messagesById = findMessageMapById(histories.map { it.mailMessageId }) + return messagesById.map { (id, message) -> + MailMessageResponse( + mailMessage = message, + mailHistories = histories.filter { it.mailMessageId == id } + ) + } + } + + fun findReservedMails(): List { + val reservations = mailReservationRepository.findByStatus(MailReservationStatus.WAITING) + val messagesById = findMessageMapById(reservations.map { it.mailMessageId }) + + return reservations + .filter { messagesById.contains(it.mailMessageId) } + .map { + MailMessageResponse( + mailMessage = messagesById.getValue(it.mailMessageId), + mailReservation = it + ) + } + } + fun reserve(request: MailData): MailMessageResponse { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val mailReservation = mailReservationRepository.save( @@ -27,4 +58,26 @@ class MailMessageService( mailReservationRepository.deleteById(mailReservation.id) mailMessageRepository.deleteById(mailReservation.mailMessageId) } + + fun sendReservedMail(standardTime: LocalDateTime = LocalDateTime.now()) { + val reservations = mailReservationRepository.findByReservationTimeBetweenAndStatus( + standardTime.minusMinutes(1), + standardTime.plusMinutes(1), + MailReservationStatus.WAITING + ) + val messagesById = findMessageMapById(reservations.map { it.mailMessageId }) + + reservations.forEach { mailReservation -> + mailReservation.send() + mailReservationRepository.save(mailReservation) + mailService.sendMailsByBccSynchronous(MailData(messagesById.getValue(mailReservation.id)), emptyMap()) + mailReservation.finish() + } + } + + private fun findMessageMapById(mailMessageIds: List): Map { + return mailMessageRepository + .findAllById(mailMessageIds) + .associateBy { it.id } + } } diff --git a/src/main/kotlin/apply/application/mail/MailReservationService.kt b/src/main/kotlin/apply/application/mail/MailReservationService.kt deleted file mode 100644 index e7619318f..000000000 --- a/src/main/kotlin/apply/application/mail/MailReservationService.kt +++ /dev/null @@ -1,39 +0,0 @@ -package apply.application.mail - -import apply.domain.mail.MailMessageRepository -import apply.domain.mail.MailReservationRepository -import apply.domain.mail.MailReservationStatus -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime - -@Transactional -@Service -class MailReservationService( - private val mailService: MailService, - private val mailReservationRepository: MailReservationRepository, - private val mailMessageRepository: MailMessageRepository -) { - fun findByWaitingStatus(): List { - return mailReservationRepository.findByStatus(MailReservationStatus.WAITING) - .map { MailReservationResponse(it) } - } - - fun sendMail(standardTime: LocalDateTime = LocalDateTime.now()) { - val reservations = mailReservationRepository.findByReservationTimeBetweenAndStatus( - standardTime.minusMinutes(1), - standardTime.plusMinutes(1), - MailReservationStatus.WAITING - ) - val messagesById = mailMessageRepository - .findAllById(reservations.map { it.mailMessageId }) - .associateBy { it.id } - - reservations.forEach { mailReservation -> - mailReservation.send() - mailReservationRepository.save(mailReservation) - mailService.sendMailsByBccSynchronous(MailData(messagesById.getValue(mailReservation.id)), emptyMap()) - mailReservation.finish() - } - } -} diff --git a/src/main/kotlin/apply/ui/api/MailReservationRestController.kt b/src/main/kotlin/apply/ui/api/MailReservationRestController.kt deleted file mode 100644 index 66a0daaab..000000000 --- a/src/main/kotlin/apply/ui/api/MailReservationRestController.kt +++ /dev/null @@ -1,23 +0,0 @@ -package apply.ui.api - -import apply.application.mail.MailReservationService -import apply.domain.user.User -import apply.security.LoginUser -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -@RequestMapping("/api/mail-reservation") -@RestController -class MailReservationRestController( - private val mailReservationService: MailReservationService -) { - @PostMapping - fun sendMail( - @LoginUser(administrator = true) user: User - ): ResponseEntity { - mailReservationService.sendMail() - return ResponseEntity.noContent().build() - } -} diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index 4feefaebe..496ad0723 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -1,6 +1,7 @@ package apply.ui.api import apply.application.mail.MailData +import apply.application.mail.MailMessageService import apply.application.mail.MailService import apply.domain.user.User import apply.security.LoginUser @@ -15,7 +16,8 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/api/mail") class MailRestController( - private val mailService: MailService + private val mailService: MailService, + private val mailMessageService: MailMessageService ) { @PostMapping fun sendMail( @@ -27,4 +29,12 @@ class MailRestController( mailService.sendMailsByBcc(request, inputStreamFiles) return ResponseEntity.noContent().build() } + + @PostMapping("/reserved") + fun sendMail( + @LoginUser(administrator = true) user: User + ): ResponseEntity { + mailMessageService.sendReservedMail() + return ResponseEntity.noContent().build() + } } diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index cc6e07af7..3f6a42485 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -6,6 +6,7 @@ import apply.createMailMessage import apply.createMailReservation import apply.domain.mail.MailMessageRepository import apply.domain.mail.MailReservationRepository +import apply.domain.mail.MailReservationStatus import apply.domain.mail.getOrThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec @@ -90,4 +91,22 @@ class MailMessageIntegrationTest( } } } + + Given("특정 시간에 발송할 예약 메일이 있는 경우") { + val reservationTime = createAvailableReservationTime() + val mailMessage1 = mailMessageRepository.save(createMailMessage()) + val mailMessage2 = mailMessageRepository.save(createMailMessage()) + + mailReservationRepository.save(createMailReservation(mailMessage1.id, reservationTime)) + mailReservationRepository.save(createMailReservation(mailMessage2.id, reservationTime.plusHours(3))) + + When("해당 시간에 메일 발송 요청을 하면") { + mailMessageService.sendReservedMail(reservationTime) + + Then("메일 전송이 완료된다") { + val actual = mailReservationRepository.findByStatus(MailReservationStatus.FINISHED) + actual.size shouldBe 1 + } + } + } }) diff --git a/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt deleted file mode 100644 index e4fbb0bc0..000000000 --- a/src/test/kotlin/apply/application/mail/MailReservationIntegrationTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package apply.application.mail - -import apply.config.TestMailConfiguration -import apply.createAvailableReservationTime -import apply.createMailMessage -import apply.createMailReservation -import apply.domain.mail.MailMessageRepository -import apply.domain.mail.MailReservationRepository -import apply.domain.mail.MailReservationStatus -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.extensions.spring.SpringTestExtension -import io.kotest.extensions.spring.SpringTestLifecycleMode -import io.kotest.matchers.shouldBe -import org.springframework.context.annotation.Import -import org.springframework.transaction.annotation.Transactional -import support.test.IntegrationTest - -@Import(TestMailConfiguration::class) -@Transactional -@IntegrationTest -class MailReservationIntegrationTest( - private val mailReservationService: MailReservationService, - private val mailReservationRepository: MailReservationRepository, - private val mailMessageRepository: MailMessageRepository -) : BehaviorSpec({ - extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) - - Given("특정 시간에 발송할 예약 메일이 있는 경우") { - val reservationTime = createAvailableReservationTime() - val mailMessage1 = mailMessageRepository.save(createMailMessage()) - val mailMessage2 = mailMessageRepository.save(createMailMessage()) - - mailReservationRepository.save(createMailReservation(mailMessage1.id, reservationTime)) - mailReservationRepository.save(createMailReservation(mailMessage2.id, reservationTime.plusHours(3))) - - When("해당 시간에 메일 발송 요청을 하면") { - mailReservationService.sendMail(reservationTime) - - Then("메일 전송이 완료된다") { - val actual = mailReservationRepository.findByStatus(MailReservationStatus.FINISHED) - actual.size shouldBe 1 - } - } - } -}) From 6f23fa743e2beec5b1722fd6c54b2df738c0a305 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Tue, 10 Oct 2023 17:02:59 +0900 Subject: [PATCH 32/40] feat: use lambda accessor to authenticate API caller --- src/main/kotlin/apply/ui/api/MailRestController.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index 496ad0723..e9b7ea277 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -4,6 +4,7 @@ import apply.application.mail.MailData import apply.application.mail.MailMessageService import apply.application.mail.MailService import apply.domain.user.User +import apply.security.Accessor import apply.security.LoginUser import org.springframework.core.io.ByteArrayResource import org.springframework.http.ResponseEntity @@ -32,7 +33,7 @@ class MailRestController( @PostMapping("/reserved") fun sendMail( - @LoginUser(administrator = true) user: User + @Accessor("lambda") ignored: Unit ): ResponseEntity { mailMessageService.sendReservedMail() return ResponseEntity.noContent().build() From 7cdbeda1a73a7bb5f463b64c9e80b96e04c0b102 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 11 Oct 2023 15:46:00 +0900 Subject: [PATCH 33/40] fix: use mailMessageId for getting MailMessage Information --- src/main/kotlin/apply/application/mail/MailMessageService.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index b7a8a3856..92833d2ee 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -70,7 +70,10 @@ class MailMessageService( reservations.forEach { mailReservation -> mailReservation.send() mailReservationRepository.save(mailReservation) - mailService.sendMailsByBccSynchronous(MailData(messagesById.getValue(mailReservation.id)), emptyMap()) + mailService.sendMailsByBccSynchronous( + MailData(messagesById.getValue(mailReservation.mailMessageId)), + emptyMap() + ) mailReservation.finish() } } From 3e96c58c07c14e229b6e43b8857ef0c8bb64e474 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Wed, 11 Oct 2023 15:48:35 +0900 Subject: [PATCH 34/40] refactor: rename MailService to SendingMailService --- .../kotlin/apply/application/mail/MailMessageService.kt | 4 ++-- .../mail/{MailService.kt => SendingMailService.kt} | 6 ++---- src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt | 6 +++--- src/main/kotlin/apply/ui/api/MailRestController.kt | 6 +++--- src/main/kotlin/apply/ui/api/UserRestController.kt | 6 +++--- .../apply/application/mail/MailServiceIntegrationTest.kt | 4 ++-- src/test/kotlin/apply/ui/api/UserRestControllerTest.kt | 8 ++++---- 7 files changed, 19 insertions(+), 21 deletions(-) rename src/main/kotlin/apply/application/mail/{MailService.kt => SendingMailService.kt} (96%) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 92833d2ee..916b64059 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -13,7 +13,7 @@ import java.time.LocalDateTime @Transactional @Service class MailMessageService( - private val mailService: MailService, + private val sendingMailService: SendingMailService, private val mailMessageRepository: MailMessageRepository, private val mailReservationRepository: MailReservationRepository, private val mailHistoryRepository: MailHistoryRepository @@ -70,7 +70,7 @@ class MailMessageService( reservations.forEach { mailReservation -> mailReservation.send() mailReservationRepository.save(mailReservation) - mailService.sendMailsByBccSynchronous( + sendingMailService.sendMailsByBccSynchronous( MailData(messagesById.getValue(mailReservation.mailMessageId)), emptyMap() ) diff --git a/src/main/kotlin/apply/application/mail/MailService.kt b/src/main/kotlin/apply/application/mail/SendingMailService.kt similarity index 96% rename from src/main/kotlin/apply/application/mail/MailService.kt rename to src/main/kotlin/apply/application/mail/SendingMailService.kt index dbcbe793c..96da44392 100644 --- a/src/main/kotlin/apply/application/mail/MailService.kt +++ b/src/main/kotlin/apply/application/mail/SendingMailService.kt @@ -11,7 +11,6 @@ import apply.domain.user.PasswordResetEvent import apply.domain.user.UserRepository import apply.domain.user.getOrThrow import org.springframework.boot.autoconfigure.mail.MailProperties -import org.springframework.context.ApplicationEventPublisher import org.springframework.core.io.ByteArrayResource import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @@ -23,7 +22,7 @@ import support.markdownToEmbeddedHtml private const val MAIL_SENDING_UNIT: Int = 50 @Service -class MailService( +class SendingMailService( private val userRepository: UserRepository, private val recruitmentRepository: RecruitmentRepository, private val mailMessageRepository: MailMessageRepository, @@ -31,8 +30,7 @@ class MailService( private val applicationProperties: ApplicationProperties, private val templateEngine: ISpringTemplateEngine, private val mailSender: MailSender, - private val mailProperties: MailProperties, - private val eventPublisher: ApplicationEventPublisher, + private val mailProperties: MailProperties ) { @Async @TransactionalEventListener diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt index ab8328275..7e82c9551 100644 --- a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt +++ b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt @@ -5,7 +5,7 @@ import apply.application.mail.MailHistoryService import apply.application.MailTargetService import apply.application.RecruitmentService import apply.application.UserService -import apply.application.mail.MailService +import apply.application.mail.SendingMailService import apply.ui.admin.BaseLayout import com.vaadin.flow.component.Component import com.vaadin.flow.component.UI @@ -32,7 +32,7 @@ class MailsFormView( evaluationService: EvaluationService, mailTargetService: MailTargetService, private val mailHistoryService: MailHistoryService, - private val mailService: MailService, + private val sendingMailService: SendingMailService, mailProperties: MailProperties ) : VerticalLayout(), HasUrlParameter { private val mailForm: MailForm = MailForm( @@ -70,7 +70,7 @@ class MailsFormView( if (result == null) { createNotification("받는사람을 한 명 이상 지정해야 합니다.") } else { - mailService.sendMailsByBcc(result, result.attachments) + sendingMailService.sendMailsByBcc(result, result.attachments) UI.getCurrent().navigate(MailsView::class.java) } } diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index e9b7ea277..05d9ab31e 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -2,7 +2,7 @@ package apply.ui.api import apply.application.mail.MailData import apply.application.mail.MailMessageService -import apply.application.mail.MailService +import apply.application.mail.SendingMailService import apply.domain.user.User import apply.security.Accessor import apply.security.LoginUser @@ -17,7 +17,7 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/api/mail") class MailRestController( - private val mailService: MailService, + private val sendingMailService: SendingMailService, private val mailMessageService: MailMessageService ) { @PostMapping @@ -27,7 +27,7 @@ class MailRestController( @LoginUser(administrator = true) user: User ): ResponseEntity { val inputStreamFiles = files.associate { (it.originalFilename!! to ByteArrayResource(it.bytes)) } - mailService.sendMailsByBcc(request, inputStreamFiles) + sendingMailService.sendMailsByBcc(request, inputStreamFiles) return ResponseEntity.noContent().build() } diff --git a/src/main/kotlin/apply/ui/api/UserRestController.kt b/src/main/kotlin/apply/ui/api/UserRestController.kt index ef09bb2a7..c65db2bf4 100644 --- a/src/main/kotlin/apply/ui/api/UserRestController.kt +++ b/src/main/kotlin/apply/ui/api/UserRestController.kt @@ -8,7 +8,7 @@ import apply.application.ResetPasswordRequest import apply.application.UserAuthenticationService import apply.application.UserResponse import apply.application.UserService -import apply.application.mail.MailService +import apply.application.mail.SendingMailService import apply.domain.user.User import apply.security.LoginUser import org.springframework.http.ResponseEntity @@ -26,7 +26,7 @@ import javax.validation.Valid class UserRestController( private val userService: UserService, private val userAuthenticationService: UserAuthenticationService, - private val mailService: MailService + private val sendingMailService: SendingMailService ) { @PostMapping("/register") fun generateToken(@RequestBody @Valid request: RegisterUserRequest): ResponseEntity> { @@ -61,7 +61,7 @@ class UserRestController( ): ResponseEntity { val authenticationCode = userAuthenticationService .generateAuthenticationCode(email) - mailService.sendAuthenticationCodeMail(email, authenticationCode) + sendingMailService.sendAuthenticationCodeMail(email, authenticationCode) return ResponseEntity.noContent().build() } diff --git a/src/test/kotlin/apply/application/mail/MailServiceIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailServiceIntegrationTest.kt index c17230b80..de6e3c088 100644 --- a/src/test/kotlin/apply/application/mail/MailServiceIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailServiceIntegrationTest.kt @@ -10,7 +10,7 @@ import support.test.IntegrationTest @Import(TestMailConfiguration::class) @IntegrationTest class MailServiceIntegrationTest( - private val mailService: MailService + private val sendingMailService: SendingMailService ) : BehaviorSpec({ Given("마크다운으로 본문을 작성한 이메일이 있는 경우") { val body = """ @@ -20,7 +20,7 @@ class MailServiceIntegrationTest( val mailData = createMailData(body = body) When("이메일 본문을 생성하면") { - val actual = mailService.generateMailBody(mailData) + val actual = sendingMailService.generateMailBody(mailData) Then("본문이 HTML로 변환된 이메일이 생성된다") { actual shouldContain "email" diff --git a/src/test/kotlin/apply/ui/api/UserRestControllerTest.kt b/src/test/kotlin/apply/ui/api/UserRestControllerTest.kt index 6978ded87..206ad654c 100644 --- a/src/test/kotlin/apply/ui/api/UserRestControllerTest.kt +++ b/src/test/kotlin/apply/ui/api/UserRestControllerTest.kt @@ -5,7 +5,7 @@ import apply.application.ResetPasswordRequest import apply.application.UserAuthenticationService import apply.application.UserResponse import apply.application.UserService -import apply.application.mail.MailService +import apply.application.mail.SendingMailService import apply.createUser import apply.domain.authenticationcode.AuthenticationCode import apply.domain.user.Gender @@ -82,13 +82,13 @@ class UserRestControllerTest : RestControllerTest() { private lateinit var userAuthenticationService: UserAuthenticationService @MockkBean - private lateinit var mailService: MailService + private lateinit var sendingMailService: SendingMailService @Test fun `유효한 회원 생성 및 검증 요청에 대하여 응답으로 토큰이 반환된다`() { val response = "valid_token" every { userAuthenticationService.generateTokenByRegister(any()) } returns response - every { mailService.sendAuthenticationCodeMail(any(), any()) } just Runs + every { sendingMailService.sendAuthenticationCodeMail(any(), any()) } just Runs mockMvc.post("/api/users/register") { jsonContent(createRegisterUserRequest()) @@ -186,7 +186,7 @@ class UserRestControllerTest : RestControllerTest() { fun `이메일 인증 코드 요청에 응답으로 NoContent를 반환한다`() { val authenticationCode = AuthenticationCode("authentication-code@email.com") every { userAuthenticationService.generateAuthenticationCode(any()) } returns authenticationCode.code - every { mailService.sendAuthenticationCodeMail(any(), any()) } just Runs + every { sendingMailService.sendAuthenticationCodeMail(any(), any()) } just Runs mockMvc.post("/api/users/authentication-code") { param("email", authenticationCode.email) From 4ba0c6f95f5b76d760168f4c4ab2ec85200b1e11 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 12 Oct 2023 14:27:37 +0900 Subject: [PATCH 35/40] refactor: edit searching condition for MailReservation --- .../kotlin/apply/application/mail/MailMessageService.kt | 5 ++--- .../kotlin/apply/domain/mail/MailReservationRepository.kt | 5 ++--- .../apply/application/mail/MailMessageIntegrationTest.kt | 6 ++++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 916b64059..6e7d6ab4c 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -60,9 +60,8 @@ class MailMessageService( } fun sendReservedMail(standardTime: LocalDateTime = LocalDateTime.now()) { - val reservations = mailReservationRepository.findByReservationTimeBetweenAndStatus( - standardTime.minusMinutes(1), - standardTime.plusMinutes(1), + val reservations = mailReservationRepository.findByReservationTimeBeforeAndStatus( + standardTime, MailReservationStatus.WAITING ) val messagesById = findMessageMapById(reservations.map { it.mailMessageId }) diff --git a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt index a27ca1aa1..c53e62a68 100644 --- a/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt +++ b/src/main/kotlin/apply/domain/mail/MailReservationRepository.kt @@ -9,9 +9,8 @@ fun MailReservationRepository.getOrThrow(id: Long) = findByIdOrNull(id) interface MailReservationRepository : JpaRepository { fun findByStatus(status: MailReservationStatus): List - fun findByReservationTimeBetweenAndStatus( - from: LocalDateTime, - to: LocalDateTime, + fun findByReservationTimeBeforeAndStatus( + standard: LocalDateTime, status: MailReservationStatus ): List diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index 3f6a42485..3c1401f85 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -96,9 +96,11 @@ class MailMessageIntegrationTest( val reservationTime = createAvailableReservationTime() val mailMessage1 = mailMessageRepository.save(createMailMessage()) val mailMessage2 = mailMessageRepository.save(createMailMessage()) + val mailMessage3 = mailMessageRepository.save(createMailMessage()) - mailReservationRepository.save(createMailReservation(mailMessage1.id, reservationTime)) - mailReservationRepository.save(createMailReservation(mailMessage2.id, reservationTime.plusHours(3))) + mailReservationRepository.save(createMailReservation(mailMessage1.id, reservationTime.minusHours(3))) + mailReservationRepository.save(createMailReservation(mailMessage2.id, reservationTime)) + mailReservationRepository.save(createMailReservation(mailMessage3.id, reservationTime.plusHours(3))) When("해당 시간에 메일 발송 요청을 하면") { mailMessageService.sendReservedMail(reservationTime) From 1c21188f22925b206e7d3d2076c5c05eee95477b Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 12 Oct 2023 14:31:37 +0900 Subject: [PATCH 36/40] refactor: add accessor key for mail scheduler --- src/main/kotlin/apply/ui/api/MailRestController.kt | 2 +- src/main/resources/application.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index 05d9ab31e..419c8c6ac 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -33,7 +33,7 @@ class MailRestController( @PostMapping("/reserved") fun sendMail( - @Accessor("lambda") ignored: Unit + @Accessor("mail-scheduler") ignored: Unit ): ResponseEntity { mailMessageService.sendReservedMail() return ResponseEntity.noContent().build() diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bbd572c1a..03d13db72 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -24,5 +24,6 @@ github.uri=https://api.github.com github.access-key= accessor.keys.lambda=LAMBDA +accessor.keys.mail-scheduler=MAIL_SCHEDULER database.initialization.excluded-table-names=flyway_schema_history From 56a5115d915f6b29e43f338c710668ac17edadbf Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 12 Oct 2023 14:43:34 +0900 Subject: [PATCH 37/40] refactor: edit name of function that send mail --- src/main/kotlin/apply/application/mail/MailMessageService.kt | 5 +---- src/main/kotlin/apply/application/mail/SendingMailService.kt | 4 ++-- src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt | 4 ++-- src/main/kotlin/apply/ui/api/MailRestController.kt | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 6e7d6ab4c..e73f605f5 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -69,10 +69,7 @@ class MailMessageService( reservations.forEach { mailReservation -> mailReservation.send() mailReservationRepository.save(mailReservation) - sendingMailService.sendMailsByBccSynchronous( - MailData(messagesById.getValue(mailReservation.mailMessageId)), - emptyMap() - ) + sendingMailService.sendByBccSynchronous(MailData(messagesById.getValue(mailReservation.mailMessageId))) mailReservation.finish() } } diff --git a/src/main/kotlin/apply/application/mail/SendingMailService.kt b/src/main/kotlin/apply/application/mail/SendingMailService.kt index 96da44392..bae8ffec3 100644 --- a/src/main/kotlin/apply/application/mail/SendingMailService.kt +++ b/src/main/kotlin/apply/application/mail/SendingMailService.kt @@ -91,7 +91,7 @@ class SendingMailService( } @Async - fun sendMailsByBcc(request: MailData, files: Map) { + fun sendByBcc(request: MailData, files: Map) { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username @@ -107,7 +107,7 @@ class SendingMailService( saveMailHistories(mailMessage.id, succeeded, failed) } - fun sendMailsByBccSynchronous(request: MailData, files: Map) { + fun sendByBccSynchronous(request: MailData, files: Map = emptyMap()) { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = mailMessage.recipients + mailProperties.username diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt index 7e82c9551..f1c657a43 100644 --- a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt +++ b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt @@ -1,10 +1,10 @@ package apply.ui.admin.mail import apply.application.EvaluationService -import apply.application.mail.MailHistoryService import apply.application.MailTargetService import apply.application.RecruitmentService import apply.application.UserService +import apply.application.mail.MailHistoryService import apply.application.mail.SendingMailService import apply.ui.admin.BaseLayout import com.vaadin.flow.component.Component @@ -70,7 +70,7 @@ class MailsFormView( if (result == null) { createNotification("받는사람을 한 명 이상 지정해야 합니다.") } else { - sendingMailService.sendMailsByBcc(result, result.attachments) + sendingMailService.sendByBcc(result, result.attachments) UI.getCurrent().navigate(MailsView::class.java) } } diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index 419c8c6ac..699f1f4d9 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -27,7 +27,7 @@ class MailRestController( @LoginUser(administrator = true) user: User ): ResponseEntity { val inputStreamFiles = files.associate { (it.originalFilename!! to ByteArrayResource(it.bytes)) } - sendingMailService.sendMailsByBcc(request, inputStreamFiles) + sendingMailService.sendByBcc(request, inputStreamFiles) return ResponseEntity.noContent().build() } From 5c5c180b02e5f8a908eaa57376dba48c1ae0bec1 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 12 Oct 2023 14:44:24 +0900 Subject: [PATCH 38/40] refactor: edit name of function that send reservation mails --- src/main/kotlin/apply/application/mail/MailMessageService.kt | 2 +- src/main/kotlin/apply/ui/api/MailRestController.kt | 2 +- .../kotlin/apply/application/mail/MailMessageIntegrationTest.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index e73f605f5..0a87b31fd 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -59,7 +59,7 @@ class MailMessageService( mailMessageRepository.deleteById(mailReservation.mailMessageId) } - fun sendReservedMail(standardTime: LocalDateTime = LocalDateTime.now()) { + fun sendReservedMails(standardTime: LocalDateTime = LocalDateTime.now()) { val reservations = mailReservationRepository.findByReservationTimeBeforeAndStatus( standardTime, MailReservationStatus.WAITING diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index 699f1f4d9..0e72e9c5c 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -35,7 +35,7 @@ class MailRestController( fun sendMail( @Accessor("mail-scheduler") ignored: Unit ): ResponseEntity { - mailMessageService.sendReservedMail() + mailMessageService.sendReservedMails() return ResponseEntity.noContent().build() } } diff --git a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt index 3c1401f85..843fd8c01 100644 --- a/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt +++ b/src/test/kotlin/apply/application/mail/MailMessageIntegrationTest.kt @@ -103,7 +103,7 @@ class MailMessageIntegrationTest( mailReservationRepository.save(createMailReservation(mailMessage3.id, reservationTime.plusHours(3))) When("해당 시간에 메일 발송 요청을 하면") { - mailMessageService.sendReservedMail(reservationTime) + mailMessageService.sendReservedMails(reservationTime) Then("메일 전송이 완료된다") { val actual = mailReservationRepository.findByStatus(MailReservationStatus.FINISHED) From dd427ec5156a7ec423706a911d7918caa94d6844 Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 12 Oct 2023 15:00:15 +0900 Subject: [PATCH 39/40] refactor: edit name of function that send mail --- src/main/kotlin/apply/application/mail/MailMessageService.kt | 2 +- src/main/kotlin/apply/application/mail/SendingMailService.kt | 4 ++-- src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt | 2 +- src/main/kotlin/apply/ui/api/MailRestController.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index 0a87b31fd..ded9d0dc9 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -69,7 +69,7 @@ class MailMessageService( reservations.forEach { mailReservation -> mailReservation.send() mailReservationRepository.save(mailReservation) - sendingMailService.sendByBccSynchronous(MailData(messagesById.getValue(mailReservation.mailMessageId))) + sendingMailService.sendMailByBccSynchronous(MailData(messagesById.getValue(mailReservation.mailMessageId))) mailReservation.finish() } } diff --git a/src/main/kotlin/apply/application/mail/SendingMailService.kt b/src/main/kotlin/apply/application/mail/SendingMailService.kt index bae8ffec3..7fb80fe52 100644 --- a/src/main/kotlin/apply/application/mail/SendingMailService.kt +++ b/src/main/kotlin/apply/application/mail/SendingMailService.kt @@ -91,7 +91,7 @@ class SendingMailService( } @Async - fun sendByBcc(request: MailData, files: Map) { + fun sendMailByBcc(request: MailData, files: Map) { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = request.recipients + mailProperties.username @@ -107,7 +107,7 @@ class SendingMailService( saveMailHistories(mailMessage.id, succeeded, failed) } - fun sendByBccSynchronous(request: MailData, files: Map = emptyMap()) { + fun sendMailByBccSynchronous(request: MailData, files: Map = emptyMap()) { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val body = generateMailBody(request) val recipients = mailMessage.recipients + mailProperties.username diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt index f1c657a43..f19c22fa8 100644 --- a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt +++ b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt @@ -70,7 +70,7 @@ class MailsFormView( if (result == null) { createNotification("받는사람을 한 명 이상 지정해야 합니다.") } else { - sendingMailService.sendByBcc(result, result.attachments) + sendingMailService.sendMailByBcc(result, result.attachments) UI.getCurrent().navigate(MailsView::class.java) } } diff --git a/src/main/kotlin/apply/ui/api/MailRestController.kt b/src/main/kotlin/apply/ui/api/MailRestController.kt index 0e72e9c5c..31b31b32f 100644 --- a/src/main/kotlin/apply/ui/api/MailRestController.kt +++ b/src/main/kotlin/apply/ui/api/MailRestController.kt @@ -27,7 +27,7 @@ class MailRestController( @LoginUser(administrator = true) user: User ): ResponseEntity { val inputStreamFiles = files.associate { (it.originalFilename!! to ByteArrayResource(it.bytes)) } - sendingMailService.sendByBcc(request, inputStreamFiles) + sendingMailService.sendMailByBcc(request, inputStreamFiles) return ResponseEntity.noContent().build() } From 0fe782957d032b9dc5bf43718396ea96bc1b480f Mon Sep 17 00:00:00 2001 From: woowabrie Date: Thu, 19 Oct 2023 09:32:48 +0900 Subject: [PATCH 40/40] refactor: change function to send reserved mail --- .../kotlin/apply/application/mail/MailMessageService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/apply/application/mail/MailMessageService.kt b/src/main/kotlin/apply/application/mail/MailMessageService.kt index ded9d0dc9..d8c172ed0 100644 --- a/src/main/kotlin/apply/application/mail/MailMessageService.kt +++ b/src/main/kotlin/apply/application/mail/MailMessageService.kt @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime -@Transactional @Service class MailMessageService( private val sendingMailService: SendingMailService, @@ -43,6 +42,7 @@ class MailMessageService( } } + @Transactional fun reserve(request: MailData): MailMessageResponse { val mailMessage = mailMessageRepository.save(request.toMailMessage()) val mailReservation = mailReservationRepository.save( @@ -51,6 +51,7 @@ class MailMessageService( return MailMessageResponse(mailMessage, mailReservation) } + @Transactional fun cancelReservation(mailMessageId: Long) { val mailReservation = mailReservationRepository.findByMailMessageId(mailMessageId) ?: throw IllegalArgumentException("메일 예약이 존재하지 않습니다. email: $mailMessageId") @@ -67,10 +68,9 @@ class MailMessageService( val messagesById = findMessageMapById(reservations.map { it.mailMessageId }) reservations.forEach { mailReservation -> - mailReservation.send() - mailReservationRepository.save(mailReservation) sendingMailService.sendMailByBccSynchronous(MailData(messagesById.getValue(mailReservation.mailMessageId))) mailReservation.finish() + mailReservationRepository.save(mailReservation) } }