Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mail): implement mail reservation #731

Open
wants to merge 40 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f89b43c
feat: add MailMessage
woowabrie Sep 14, 2023
559f00a
feat: add MailReservation
woowabrie Sep 26, 2023
faa24f5
feat: add MailHistory
woowabrie Oct 4, 2023
81f6188
feat: mapping relationships with mail message and reservation
woowabrie Oct 4, 2023
ad730d6
feat: mapping relationships with mail message and history
woowabrie Oct 4, 2023
742c8a6
feat: using new MailHistory
woowabrie Oct 4, 2023
1d6032b
refactor: delete old MailHistory
woowabrie Oct 4, 2023
d09c01b
feat: Add functionality for scheduling email reservations
woowabrie Oct 4, 2023
b8168e6
feat: add functionality for updating mail messages
woowabrie Oct 4, 2023
f12edfa
feat: add functionality for updating mail's reservation time
woowabrie Oct 4, 2023
f5777a4
feat: add functionality for deleting mail's reservation
woowabrie Oct 4, 2023
9e29d51
feat: add functionality for finding mail's reservation
woowabrie Oct 4, 2023
501928e
refactor: create MailHistory when MailSentEvent is published
woowabrie Oct 5, 2023
e914ed2
feat: add endpoint to send reservation mail
woowabrie Oct 5, 2023
f766d5d
refactor: migrate sql relating with mail tables
woowabrie Oct 5, 2023
3b6cd96
fix: edit searching conditions for mail reservations
woowabrie Oct 7, 2023
ba9d7b0
feat: remove functions relates with updating mail message
woowabrie Oct 7, 2023
16e0a74
feat: remove functions relates with updating reservationTime of mail
woowabrie Oct 7, 2023
e2d7e91
refactor: extract MailReservationStatus into another file
woowabrie Oct 9, 2023
0a673d4
refactor: adjust order of annotation on MailReservationController
woowabrie Oct 9, 2023
ee17685
refactor: rename functions changing status of reservation
woowabrie Oct 9, 2023
1d11f1b
refactor: delete factory method from MailHistory
woowabrie Oct 9, 2023
2c3341f
test: edit typo for creatorId of MailMessage
woowabrie Oct 7, 2023
f0257c5
refactor: delete factory method from MailMessage
woowabrie Oct 9, 2023
fb64635
refactor: make relationships with MailHistory and MailMessage by id r…
woowabrie Oct 9, 2023
237ba8c
refactor: make relationships with MailReservation and MailMessage by …
woowabrie Oct 9, 2023
4bf23cb
refactor: add synchronous function to send mail
woowabrie Oct 9, 2023
19cee58
refactor: delete MailSentEvent and Listener
woowabrie Oct 9, 2023
8bf6500
test: add test for sending reservation mail
woowabrie Oct 10, 2023
f2760b4
refactor: extract functions to get reservation time
woowabrie Oct 10, 2023
49f91f6
refactor: move mails functions into MailMessageService
woowabrie Oct 10, 2023
6f23fa7
feat: use lambda accessor to authenticate API caller
woowabrie Oct 10, 2023
7cdbeda
fix: use mailMessageId for getting MailMessage Information
woowabrie Oct 11, 2023
3e96c58
refactor: rename MailService to SendingMailService
woowabrie Oct 11, 2023
4ba0c6f
refactor: edit searching condition for MailReservation
woowabrie Oct 12, 2023
1c21188
refactor: add accessor key for mail scheduler
woowabrie Oct 12, 2023
56a5115
refactor: edit name of function that send mail
woowabrie Oct 12, 2023
5c5c180
refactor: edit name of function that send reservation mails
woowabrie Oct 12, 2023
dd427ec
refactor: edit name of function that send mail
woowabrie Oct 12, 2023
0fe7829
refactor: change function to send reserved mail
woowabrie Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions src/main/kotlin/apply/application/MailHistoryService.kt

This file was deleted.

22 changes: 18 additions & 4 deletions src/main/kotlin/apply/application/mail/MailData.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apply.application.mail

import apply.domain.mail.MailHistory
import apply.domain.mail.MailMessage
import org.springframework.core.io.ByteArrayResource
import java.time.LocalDateTime
import javax.validation.constraints.NotEmpty
Expand Down Expand Up @@ -30,12 +31,25 @@ data class MailData(
@field:NotNull
var id: Long = 0L
) {
constructor(mailHistory: MailHistory) : this(
mailHistory.subject,
mailHistory.body,
mailHistory.sender,
constructor(mailMessage: MailMessage) : this(
mailMessage.subject,
mailMessage.body,
mailMessage.sender,
mailMessage.recipients,
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)
}
}
65 changes: 65 additions & 0 deletions src/main/kotlin/apply/application/mail/MailDtos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package apply.application.mail

import apply.domain.mail.MailHistory
import apply.domain.mail.MailMessage
import apply.domain.mail.MailReservation
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<String>,
val createdDateTime: LocalDateTime,
val sentTime: LocalDateTime?,
val reservation: MailReservationResponse?,
val histories: List<MailHistoryResponse>
) {
constructor(
mailMessage: MailMessage,
mailReservation: MailReservation? = null,
mailHistories: List<MailHistory> = emptyList()
) : this(
mailMessage.id,
mailMessage.subject,
mailMessage.body,
mailMessage.sender,
mailMessage.recipients,
mailMessage.createdDateTime,
mailHistories.firstOrNull()?.sentTime,
mailReservation?.let { MailReservationResponse(it) },
mailHistories.map { MailHistoryResponse(it) }
)
}

data class MailReservationResponse(
val id: Long,
val mailMessageId: Long,
val status: MailReservationStatus,
val reservationTime: LocalDateTime
) {
constructor(mailReservation: MailReservation) : this(
mailReservation.id,
mailReservation.mailMessageId,
mailReservation.status,
mailReservation.reservationTime
)
}

data class MailHistoryResponse(
val id: Long,
val mailMessageId: Long,
val recipients: List<String>,
val success: Boolean,
val sentTime: LocalDateTime
) {
constructor(mailHistory: MailHistory) : this(
mailHistory.id,
mailHistory.mailMessageId,
mailHistory.recipients,
mailHistory.success,
mailHistory.sentTime
)
}
26 changes: 26 additions & 0 deletions src/main/kotlin/apply/application/mail/MailHistoryService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package apply.application.mail

import apply.domain.mail.MailHistoryRepository
import apply.domain.mail.MailMessageRepository
import apply.domain.mail.getOrThrow
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Transactional
@Service
class MailHistoryService(
private val mailHistoryRepository: MailHistoryRepository,
private val mailMessageRepository: MailMessageRepository
) {
fun findAll(): List<MailData> {
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)
val mailMessage = mailMessageRepository.getOrThrow(mailHistory.mailMessageId)
return MailData(mailMessage, mailHistory)
}
}
82 changes: 82 additions & 0 deletions src/main/kotlin/apply/application/mail/MailMessageService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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

@Service
class MailMessageService(
private val sendingMailService: SendingMailService,
private val mailMessageRepository: MailMessageRepository,
private val mailReservationRepository: MailReservationRepository,
private val mailHistoryRepository: MailHistoryRepository
) {
fun findSentMails(): List<MailMessageResponse> {
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<MailMessageResponse> {
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
)
}
}

@Transactional
fun reserve(request: MailData): MailMessageResponse {
val mailMessage = mailMessageRepository.save(request.toMailMessage())
val mailReservation = mailReservationRepository.save(
MailReservation(mailMessageId = mailMessage.id, reservationTime = request.sentTime)
)
return MailMessageResponse(mailMessage, mailReservation)
}

@Transactional
fun cancelReservation(mailMessageId: Long) {
val mailReservation = mailReservationRepository.findByMailMessageId(mailMessageId)
?: throw IllegalArgumentException("메일 예약이 존재하지 않습니다. email: $mailMessageId")
check(mailReservation.canCancel()) { "예약 취소할 수 없는 메일입니다." }
mailReservationRepository.deleteById(mailReservation.id)
mailMessageRepository.deleteById(mailReservation.mailMessageId)
}

fun sendReservedMails(standardTime: LocalDateTime = LocalDateTime.now()) {
val reservations = mailReservationRepository.findByReservationTimeBeforeAndStatus(
standardTime,
MailReservationStatus.WAITING
)
val messagesById = findMessageMapById(reservations.map { it.mailMessageId })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MailReservation이랑 MailMessage를 한 번에 불러오면 쿼리 호출 횟수도 줄이고 코드 복잡도도 줄어들거 같아요
아래 예시 쿼리로 작성했어요. JPQL 문법이 맞는지 확인이 필요합니다 ㅎㅎ;

    @Query("select m" +
        " from MailMessage m" +
        " join MailReservation ms on ms.mailMessageId = m.id" +
        " where ms.reservationTime between :from and :to" +
        " and ms.status = :status")
    fun findByReservationTimeBetweenAndStatus(
        from: LocalDateTime,
        to: LocalDateTime,
        status: MailReservationStatus
    ): List<MailMessage>

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이렇게하면 바로 예약 MailMessage 를 가져올 수 있군요! 넘 좋습니다 ㅎㅎㅎ
한 가지 궁금한게 이 때 조회한 MailReservation도 함께 받을 수 있나요? 메일 전송만 생각하면 MailMessage만 있어도 되는데, 상태 변경이 필요해서요!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿼리에서 select에 ms를 추가하면 될거에요. 그런데 MailMessage가 MailReservation를 간접참조 하고 있으면 좀 더 수정해야 해요. dto를 새로 만들거나 객체를 참조하도록 바꾸는 방법이 있겠네요


reservations.forEach { mailReservation ->

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 해당 메일 발송 작업이 하나의 트랜잭션으로 묶여야 할 이유가 있을까요? 개별적으로 성공 실패가 나뉘는게 더 자연스럽지 않을까 합니다.

sendingMailService.sendMailByBccSynchronous(MailData(messagesById.getValue(mailReservation.mailMessageId)))
mailReservation.finish()
mailReservationRepository.save(mailReservation)
}
}

private fun findMessageMapById(mailMessageIds: List<Long>): Map<Long, MailMessage> {
return mailMessageRepository
.findAllById(mailMessageIds)
.associateBy { it.id }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.recruitment.RecruitmentRepository
import apply.domain.recruitment.getOrThrow
import apply.domain.user.PasswordResetEvent
Expand All @@ -21,9 +22,10 @@ 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,
private val mailHistoryRepository: MailHistoryRepository,
private val applicationProperties: ApplicationProperties,
private val templateEngine: ISpringTemplateEngine,
Expand Down Expand Up @@ -89,11 +91,11 @@ class MailService(
}

@Async
fun sendMailsByBcc(request: MailData, files: Map<String, ByteArrayResource>) {
fun sendMailByBcc(request: MailData, files: Map<String, ByteArrayResource>) {
val mailMessage = mailMessageRepository.save(request.toMailMessage())
val body = generateMailBody(request)
val recipients = request.recipients + mailProperties.username

// TODO: 성공과 실패를 분리하여 히스토리 관리
val succeeded = mutableListOf<String>()
val failed = mutableListOf<String>()
for (addresses in recipients.chunked(MAIL_SENDING_UNIT)) {
Expand All @@ -102,15 +104,23 @@ class MailService(
.onFailure { failed.addAll(addresses) }
}

mailHistoryRepository.save(
MailHistory(
request.subject,
request.body,
request.sender,
request.recipients,
request.sentTime
)
)
saveMailHistories(mailMessage.id, succeeded, failed)
}

fun sendMailByBccSynchronous(request: MailData, files: Map<String, ByteArrayResource> = emptyMap()) {
val mailMessage = mailMessageRepository.save(request.toMailMessage())
val body = generateMailBody(request)
val recipients = mailMessage.recipients + mailProperties.username

val succeeded = mutableListOf<String>()
val failed = mutableListOf<String>()
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 {
Expand All @@ -124,4 +134,22 @@ class MailService(
}
return templateEngine.process("mail/common", context)
}

private fun saveMailHistories(
mailMessageId: Long,
succeeded: MutableList<String>,
failed: MutableList<String>
) {
val mailHistories = mutableListOf<MailHistory>()

if (succeeded.isNotEmpty()) {
mailHistories.add(MailHistory(mailMessageId, succeeded, true))
}

if (failed.isNotEmpty()) {
mailHistories.add(MailHistory(mailMessageId, failed, false))
}

mailHistoryRepository.saveAll(mailHistories)
}
}
25 changes: 20 additions & 5 deletions src/main/kotlin/apply/config/DatabaseInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import apply.domain.judgmentitem.JudgmentItemRepository
import apply.domain.judgmentitem.ProgrammingLanguage
import apply.domain.mail.MailHistory
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
Expand Down Expand Up @@ -57,6 +59,7 @@ class DatabaseInitializer(
private val missionRepository: MissionRepository,
private val judgmentItemRepository: JudgmentItemRepository,
private val assignmentRepository: AssignmentRepository,
private val mailMessageRepository: MailMessageRepository,
private val mailHistoryRepository: MailHistoryRepository,
private val database: Database
) : CommandLineRunner {
Expand Down Expand Up @@ -429,13 +432,25 @@ class DatabaseInitializer(
}

private fun populateMailHistories() {
val mailMessage = MailMessage(
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)
mailMessageId = mailMessage.id,
recipients = mailMessage.recipients.subList(0, 2),
success = true
),
MailHistory(
mailMessageId = mailMessage.id,
recipients = mailMessage.recipients.subList(3, 4),
success = true
)
)
mailHistoryRepository.saveAll(mailHistories)
Expand Down
12 changes: 4 additions & 8 deletions src/main/kotlin/apply/domain/mail/MailHistory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,16 @@ import javax.persistence.Lob
@Entity
class MailHistory(
@Column(nullable = false)
val subject: String,

@Column(nullable = false)
@Lob
val body: String,

@Column(nullable = false)
val sender: String,
val mailMessageId: Long = 0L,

@Column(nullable = false)
@Convert(converter = StringToListConverter::class)
@Lob
val recipients: List<String>,

@Column(nullable = false)
val success: Boolean,

@Column(nullable = false)
val sentTime: LocalDateTime = LocalDateTime.now(),
id: Long = 0L
Expand Down
Loading