diff --git a/src/main/kotlin/apply/application/EvaluationDtos.kt b/src/main/kotlin/apply/application/EvaluationDtos.kt index 161156681..4597c633b 100644 --- a/src/main/kotlin/apply/application/EvaluationDtos.kt +++ b/src/main/kotlin/apply/application/EvaluationDtos.kt @@ -4,6 +4,7 @@ import apply.domain.evaluation.Evaluation import apply.domain.evaluationItem.EvaluationItem import apply.domain.evaluationtarget.EvaluationStatus import apply.domain.recruitment.Recruitment +import apply.domain.user.User import javax.validation.Valid import javax.validation.constraints.Max import javax.validation.constraints.Min @@ -150,10 +151,11 @@ data class EvaluationTargetData( ) data class MailTargetResponse( - val name: String, - val email: String + val email: String, + val name: String? = null ) { - constructor(userResponse: UserResponse) : this(userResponse.name, userResponse.email) + constructor(userResponse: UserResponse) : this(userResponse.email, userResponse.name) + constructor(user: User) : this(user.email, user.name) } data class EvaluationItemScoreData( diff --git a/src/main/kotlin/apply/application/MailHistoryService.kt b/src/main/kotlin/apply/application/MailHistoryService.kt new file mode 100644 index 000000000..138ccefe4 --- /dev/null +++ b/src/main/kotlin/apply/application/MailHistoryService.kt @@ -0,0 +1,37 @@ +package apply.application + +import apply.application.mail.MailData +import apply.domain.mail.MailHistory +import apply.domain.mail.MailHistoryRepository +import apply.domain.mail.getById +import apply.domain.user.UserRepository +import org.springframework.stereotype.Service +import javax.transaction.Transactional + +@Transactional +@Service +class MailHistoryService( + private val mailHistoryRepository: MailHistoryRepository, + private val userRepository: UserRepository +) { + fun save(request: MailData) { + mailHistoryRepository.save( + MailHistory( + request.subject, + request.body, + request.sender, + request.recipients, + request.sentTime + ) + ) + } + + fun findAll(): List { + return mailHistoryRepository.findAll().map { MailData(it) } + } + + fun getById(mailHistoryId: Long): MailData { + val mailHistory = mailHistoryRepository.getById(mailHistoryId) + return MailData(mailHistory) + } +} diff --git a/src/main/kotlin/apply/application/MailTargetService.kt b/src/main/kotlin/apply/application/MailTargetService.kt index 853ba606c..474734459 100644 --- a/src/main/kotlin/apply/application/MailTargetService.kt +++ b/src/main/kotlin/apply/application/MailTargetService.kt @@ -4,6 +4,7 @@ import apply.domain.evaluationtarget.EvaluationStatus import apply.domain.evaluationtarget.EvaluationTarget import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.user.UserRepository +import apply.domain.user.findAllByEmailIn import org.springframework.stereotype.Service import javax.transaction.Transactional @@ -16,7 +17,13 @@ class MailTargetService( fun findMailTargets(evaluationId: Long, evaluationStatus: EvaluationStatus? = null): List { val userIds = findEvaluationTargets(evaluationId, evaluationStatus).map { it.userId } return userRepository.findAllById(userIds) - .map { MailTargetResponse(it.name, it.email) } + .map { MailTargetResponse(it.email, it.name) } + } + + fun findAllByEmails(emails: List): List { + val users = userRepository.findAllByEmailIn(emails) + val anonymousEmails = emails - users.map { it.email } + return users.map { MailTargetResponse(it) } + anonymousEmails.map { MailTargetResponse(it) } } private fun findEvaluationTargets(evaluationId: Long, evaluationStatus: EvaluationStatus?): List { diff --git a/src/main/kotlin/apply/application/mail/MailData.kt b/src/main/kotlin/apply/application/mail/MailData.kt index 36ca705a2..63227dc3f 100644 --- a/src/main/kotlin/apply/application/mail/MailData.kt +++ b/src/main/kotlin/apply/application/mail/MailData.kt @@ -1,14 +1,38 @@ package apply.application.mail +import apply.domain.mail.MailHistory +import java.time.LocalDateTime import javax.validation.constraints.NotEmpty +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size data class MailData( @field:NotEmpty + @field:Size(min = 1, max = 100) var subject: String = "", @field:NotEmpty var body: String = "", @field:NotEmpty - var recipients: List = emptyList() -) + @field:Size(min = 1, max = 100) + var sender: String = "", + + @field:NotEmpty + var recipients: List = emptyList(), + + @field:NotNull + var sentTime: LocalDateTime = LocalDateTime.now(), + + @field:NotNull + var id: Long = 0L +) { + constructor(mailHistory: MailHistory) : this( + mailHistory.subject, + mailHistory.body, + mailHistory.sender, + mailHistory.recipients, + mailHistory.sentTime, + mailHistory.id + ) +} diff --git a/src/main/kotlin/apply/domain/mail/MailHistory.kt b/src/main/kotlin/apply/domain/mail/MailHistory.kt new file mode 100644 index 000000000..40f9e8c12 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailHistory.kt @@ -0,0 +1,31 @@ +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.Lob + +@Entity +class MailHistory( + @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 sentTime: LocalDateTime = LocalDateTime.now(), + id: Long = 0L +) : BaseEntity(id) diff --git a/src/main/kotlin/apply/domain/mail/MailHistoryRepository.kt b/src/main/kotlin/apply/domain/mail/MailHistoryRepository.kt new file mode 100644 index 000000000..a6e60b638 --- /dev/null +++ b/src/main/kotlin/apply/domain/mail/MailHistoryRepository.kt @@ -0,0 +1,8 @@ +package apply.domain.mail + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun MailHistoryRepository.getById(id: Long): MailHistory = findByIdOrNull(id) ?: throw NoSuchElementException() + +interface MailHistoryRepository : JpaRepository diff --git a/src/main/kotlin/apply/ui/admin/BaseLayout.kt b/src/main/kotlin/apply/ui/admin/BaseLayout.kt index b4705469c..03b9dac61 100644 --- a/src/main/kotlin/apply/ui/admin/BaseLayout.kt +++ b/src/main/kotlin/apply/ui/admin/BaseLayout.kt @@ -3,7 +3,7 @@ package apply.ui.admin import apply.application.RecruitmentService import apply.ui.admin.cheater.CheatersView import apply.ui.admin.evaluation.EvaluationsView -import apply.ui.admin.mail.MailFormView +import apply.ui.admin.mail.MailsView import apply.ui.admin.recruitment.RecruitmentsView import com.vaadin.flow.component.Component import com.vaadin.flow.component.applayout.AppLayout @@ -58,7 +58,7 @@ class BaseLayout( "과제 관리".accordionOf("admin/missions", recruitments), "선발 과정".accordionOf("admin/selections", recruitments), "부정 행위자" of CheatersView::class.java, - "메일 발송" of MailFormView::class.java + "메일 관리" of MailsView::class.java ) } } diff --git a/src/main/kotlin/apply/ui/admin/mail/GroupMailTargetDialog.kt b/src/main/kotlin/apply/ui/admin/mail/GroupMailTargetDialog.kt index c859f090f..9f1bf39e3 100644 --- a/src/main/kotlin/apply/ui/admin/mail/GroupMailTargetDialog.kt +++ b/src/main/kotlin/apply/ui/admin/mail/GroupMailTargetDialog.kt @@ -16,6 +16,7 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent import com.vaadin.flow.component.orderedlayout.HorizontalLayout import com.vaadin.flow.component.select.Select import com.vaadin.flow.data.provider.ListDataProvider +import support.views.NO_NAME import support.views.addSortableColumn import support.views.createContrastButton import support.views.createItemSelect @@ -39,7 +40,7 @@ class GroupMailTargetDialog( private fun createMailTargetsGrid(): Grid { return Grid(10).apply { - addSortableColumn("이름", MailTargetResponse::name) + addSortableColumn("이름") { it.name ?: NO_NAME } addSortableColumn("이메일", MailTargetResponse::email) } } diff --git a/src/main/kotlin/apply/ui/admin/mail/MailFormView.kt b/src/main/kotlin/apply/ui/admin/mail/MailForm.kt similarity index 56% rename from src/main/kotlin/apply/ui/admin/mail/MailFormView.kt rename to src/main/kotlin/apply/ui/admin/mail/MailForm.kt index 8ce5978fc..4054b64fe 100644 --- a/src/main/kotlin/apply/ui/admin/mail/MailFormView.kt +++ b/src/main/kotlin/apply/ui/admin/mail/MailForm.kt @@ -6,34 +6,27 @@ import apply.application.MailTargetService import apply.application.RecruitmentService import apply.application.UserService import apply.application.mail.MailData -import apply.ui.admin.BaseLayout import com.vaadin.flow.component.Component -import com.vaadin.flow.component.UI import com.vaadin.flow.component.button.Button import com.vaadin.flow.component.grid.Grid import com.vaadin.flow.component.orderedlayout.FlexComponent import com.vaadin.flow.component.orderedlayout.HorizontalLayout -import com.vaadin.flow.component.orderedlayout.VerticalLayout import com.vaadin.flow.component.textfield.TextArea import com.vaadin.flow.component.textfield.TextField +import com.vaadin.flow.component.upload.Upload import com.vaadin.flow.component.upload.receivers.MultiFileMemoryBuffer import com.vaadin.flow.data.renderer.ComponentRenderer import com.vaadin.flow.data.renderer.Renderer -import com.vaadin.flow.router.Route import org.springframework.boot.autoconfigure.mail.MailProperties import support.views.BindingFormLayout import support.views.NO_NAME -import support.views.Title import support.views.addSortableColumn -import support.views.createContrastButton import support.views.createEnterBox import support.views.createErrorSmallButton import support.views.createNormalButton -import support.views.createPrimaryButton import support.views.createUpload -@Route(value = "admin/mails", layout = BaseLayout::class) -class MailFormView( +class MailForm( private val userService: UserService, private val recruitmentService: RecruitmentService, private val evaluationService: EvaluationService, @@ -44,33 +37,14 @@ class MailFormView( private val body: TextArea = createBody() private val mailTargets: MutableSet = mutableSetOf() private val mailTargetsGrid: Grid = createMailTargetsGrid(mailTargets) + private val recipientFilter: Component = createRecipientFilter() + private val fileUpload: Component = createFileUpload() init { - add(Title("메일 발송"), createMailForm()) + add(subject, createSender(), recipientFilter, mailTargetsGrid, body, fileUpload) setResponsiveSteps(ResponsiveStep("0", 1)) drawRequired() - } - - private fun createMailForm(): Component { - return VerticalLayout( - subject, - createSender(), - createRecipientFilter(), - mailTargetsGrid, - body, - createUpload("파일첨부", MultiFileMemoryBuffer()) { - // TODO: 추후 업로드 된 파일을 메일로 첨부하는 로직이 추가되어야 함 - // (uploadFiles 같은 필드를 두고 mail을 보내는 기능에 포함시키면 될 것 같음) - // it.files.forEach { fileName -> - // val fileData = it.getFileData(fileName) - // val inputStream = it.getInputStream(fileName) - // val readBytes = inputStream.readBytes() - // } - }, - createButtons() - ).apply { - setSizeFull() - } + refreshGridFooter() } private fun createSender(): Component { @@ -82,29 +56,16 @@ class MailFormView( private fun createRecipientFilter(): Component { return HorizontalLayout( - createEnterBox(labelText = "받는사람") { - if (it.isNotBlank()) { - mailTargets.addAndRefresh(MailTargetResponse(NO_NAME, it)) - } - }, + createTargetEnterBox(), createIndividualLoadButton(), createGroupLoadButton() ).apply { defaultVerticalComponentAlignment = FlexComponent.Alignment.END } } - private fun createMailTargetsGrid(mailTargets: Set): Grid { - return Grid(10).apply { - addSortableColumn("이름", MailTargetResponse::name) - addSortableColumn("이메일", MailTargetResponse::email) - addColumn(createRemoveButton()) - setItems(mailTargets) - } - } - - private fun createRemoveButton(): Renderer { - return ComponentRenderer { response -> - createErrorSmallButton("제거") { - mailTargets.removeAndRefresh(response) + private fun createTargetEnterBox(): Component { + return createEnterBox("받는사람") { + if (it.isNotBlank()) { + refreshGrid { mailTargets.add(MailTargetResponse(it, NO_NAME)) } } } } @@ -112,7 +73,7 @@ class MailFormView( private fun createIndividualLoadButton(): Button { return createNormalButton("개별 불러오기") { IndividualMailTargetDialog(userService) { - mailTargets.addAndRefresh(it) + refreshGrid { mailTargets.add(it) } } } } @@ -120,11 +81,20 @@ class MailFormView( private fun createGroupLoadButton(): Component { return createNormalButton("그룹 불러오기") { GroupMailTargetDialog(recruitmentService, evaluationService, mailTargetService) { - mailTargets.addAllAndRefresh(it) + refreshGrid { mailTargets.addAll(it) } } } } + private fun createMailTargetsGrid(mailTargets: Set): Grid { + return Grid(10).apply { + addSortableColumn("이름") { it.name ?: NO_NAME } + addSortableColumn("이메일", MailTargetResponse::email) + addColumn(createRemoveButton()) + setItems(mailTargets) + } + } + private fun createBody(): TextArea { return TextArea("본문").apply { setSizeFull() @@ -132,47 +102,53 @@ class MailFormView( } } - private fun createButtons(): Component { - return HorizontalLayout(createSubmitButton(), createCancelButton()).apply { - setSizeFull() - justifyContentMode = FlexComponent.JustifyContentMode.CENTER + private fun createFileUpload(): Upload { + return createUpload("파일첨부", MultiFileMemoryBuffer()) { + // TODO: 추후 업로드 된 파일을 메일로 첨부하는 로직이 추가되어야 함 + // (uploadFiles 같은 필드를 두고 mail을 보내는 기능에 포함시키면 될 것 같음) + // it.files.forEach { fileName -> + // val fileData = it.getFileData(fileName) + // val inputStream = it.getInputStream(fileName) + // val readBytes = inputStream.readBytes() + // } } } - private fun createSubmitButton(): Button { - return createPrimaryButton("보내기") { - bindOrNull()?.let { - // TODO: emailService.메일전송(it, uploadFile) - UI.getCurrent().navigate(MailFormView::class.java) + private fun createRemoveButton(): Renderer { + return ComponentRenderer { response -> + createErrorSmallButton("제거") { + refreshGrid { mailTargets.remove(response) } } } } - private fun createCancelButton(): Button { - return createContrastButton("취소") { - UI.getCurrent().navigate(MailFormView::class.java) + override fun bindOrNull(): MailData? { + return bindDefaultOrNull()?.apply { + recipients = mailTargets.map { it.email }.toList() } } - private fun MutableSet.addAndRefresh(element: MailTargetResponse) { - add(element).also { mailTargetsGrid.dataProvider.refreshAll() } - } - - private fun MutableSet.addAllAndRefresh(elements: Collection) { - addAll(elements).also { mailTargetsGrid.dataProvider.refreshAll() } + override fun fill(data: MailData) { + fillDefault(data) + toReadOnlyMode() + refreshGrid { mailTargets.addAll(mailTargetService.findAllByEmails(data.recipients)) } } - private fun MutableSet.removeAndRefresh(element: MailTargetResponse) { - remove(element).also { mailTargetsGrid.dataProvider.refreshAll() } + private fun toReadOnlyMode() { + subject.isReadOnly = true + body.isReadOnly = true + mailTargetsGrid.columns.last().isVisible = false + recipientFilter.isVisible = false + fileUpload.isVisible = false } - override fun bindOrNull(): MailData? { - return bindDefaultOrNull()?.apply { - recipients = mailTargets.map { it.email }.toList() - } + private fun refreshGrid(action: MutableSet.() -> Unit = {}) { + mailTargets.action() + mailTargetsGrid.dataProvider.refreshAll() + refreshGridFooter() } - override fun fill(data: MailData) { - fillDefault(data) + private fun refreshGridFooter() { + mailTargetsGrid.columns.first().setFooter("받는사람: ${mailTargets.size}명") } } diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt new file mode 100644 index 000000000..b4f71ad2b --- /dev/null +++ b/src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt @@ -0,0 +1,79 @@ +package apply.ui.admin.mail + +import apply.application.EvaluationService +import apply.application.MailHistoryService +import apply.application.MailTargetService +import apply.application.RecruitmentService +import apply.application.UserService +import apply.ui.admin.BaseLayout +import com.vaadin.flow.component.Component +import com.vaadin.flow.component.UI +import com.vaadin.flow.component.button.Button +import com.vaadin.flow.component.orderedlayout.FlexComponent +import com.vaadin.flow.component.orderedlayout.HorizontalLayout +import com.vaadin.flow.component.orderedlayout.VerticalLayout +import com.vaadin.flow.router.BeforeEvent +import com.vaadin.flow.router.HasUrlParameter +import com.vaadin.flow.router.Route +import com.vaadin.flow.router.WildcardParameter +import org.springframework.boot.autoconfigure.mail.MailProperties +import support.views.EDIT_VALUE +import support.views.FORM_URL_PATTERN +import support.views.Title +import support.views.createContrastButton +import support.views.createPrimaryButton + +@Route(value = "admin/mails", layout = BaseLayout::class) +class MailsFormView( + userService: UserService, + recruitmentService: RecruitmentService, + evaluationService: EvaluationService, + mailTargetService: MailTargetService, + private val mailHistoryService: MailHistoryService, + mailProperties: MailProperties +) : VerticalLayout(), HasUrlParameter { + private val mailForm: MailForm = MailForm( + userService, + recruitmentService, + evaluationService, + mailTargetService, + mailProperties + ) + private val submitButton: Component = createSubmitButton() + + init { + add(Title("메일"), mailForm, createButtons()) + } + + override fun setParameter(event: BeforeEvent, @WildcardParameter parameter: String) { + val result = FORM_URL_PATTERN.find(parameter) ?: return UI.getCurrent().page.history.back() + val (id, value) = result.destructured + if (value == EDIT_VALUE) { + mailForm.fill(mailHistoryService.getById(id.toLong())) + submitButton.isVisible = false + } + } + + private fun createButtons(): Component { + return HorizontalLayout(submitButton, createCancelButton()).apply { + setSizeFull() + justifyContentMode = FlexComponent.JustifyContentMode.CENTER + } + } + + private fun createSubmitButton(): Button { + return createPrimaryButton("보내기") { + mailForm.bindOrNull()?.let { + mailHistoryService.save(it) + // TODO: emailService.메일전송(it, uploadFile) + UI.getCurrent().navigate(MailsView::class.java) + } + } + } + + private fun createCancelButton(): Button { + return createContrastButton("취소") { + UI.getCurrent().navigate(MailsView::class.java) + } + } +} diff --git a/src/main/kotlin/apply/ui/admin/mail/MailsView.kt b/src/main/kotlin/apply/ui/admin/mail/MailsView.kt new file mode 100644 index 000000000..a66e6b0de --- /dev/null +++ b/src/main/kotlin/apply/ui/admin/mail/MailsView.kt @@ -0,0 +1,68 @@ +package apply.ui.admin.mail + +import apply.application.MailHistoryService +import apply.application.mail.MailData +import apply.ui.admin.BaseLayout +import com.vaadin.flow.component.Component +import com.vaadin.flow.component.UI +import com.vaadin.flow.component.grid.Grid +import com.vaadin.flow.component.html.H1 +import com.vaadin.flow.component.orderedlayout.FlexComponent +import com.vaadin.flow.component.orderedlayout.HorizontalLayout +import com.vaadin.flow.component.orderedlayout.VerticalLayout +import com.vaadin.flow.data.renderer.ComponentRenderer +import com.vaadin.flow.data.renderer.Renderer +import com.vaadin.flow.router.Route +import support.views.EDIT_VALUE +import support.views.NEW_VALUE +import support.views.addSortableColumn +import support.views.addSortableDateTimeColumn +import support.views.createPrimaryButton +import support.views.createPrimarySmallButton + +@Route(value = "admin/mails", layout = BaseLayout::class) +class MailsView( + private val mailHistoryService: MailHistoryService +) : VerticalLayout() { + init { + add(createTitle(), createButton(), createGrid()) + } + + private fun createTitle(): Component { + return HorizontalLayout(H1("메일 관리")).apply { + setSizeFull() + justifyContentMode = FlexComponent.JustifyContentMode.CENTER + } + } + + private fun createButton(): Component { + return HorizontalLayout( + createPrimaryButton("메일 보내기") { + UI.getCurrent().navigate(MailsFormView::class.java, NEW_VALUE) + } + ).apply { + setSizeFull() + justifyContentMode = FlexComponent.JustifyContentMode.END + } + } + + private fun createGrid(): Component { + return Grid(10).apply { + addSortableColumn("메일 제목", MailData::subject) + addSortableDateTimeColumn("보낸 시간", MailData::sentTime) + addSortableColumn("받는사람 수") { "${it.recipients.size}명" } + addColumn(createButtonRenderer()).apply { isAutoWidth = true } + setItems(mailHistoryService.findAll()) + } + } + + private fun createButtonRenderer(): Renderer { + return ComponentRenderer { response -> createDetailButton(response) } + } + + private fun createDetailButton(mailData: MailData): Component { + return createPrimarySmallButton("상세 보기") { + UI.getCurrent().navigate(MailsFormView::class.java, "${mailData.id}/$EDIT_VALUE") + } + } +} diff --git a/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt b/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt new file mode 100644 index 000000000..bae64586d --- /dev/null +++ b/src/main/kotlin/apply/ui/api/MailHistoryRestController.kt @@ -0,0 +1,35 @@ +package apply.ui.api + +import apply.application.MailHistoryService +import apply.application.mail.MailData +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import javax.validation.Valid + +@RestController +@RequestMapping("/api/mail-history") +class MailHistoryRestController( + private val mailHistoryService: MailHistoryService +) { + @PostMapping + fun save(@RequestBody @Valid request: MailData): ResponseEntity { + // todo: 파일 첨부하여 보내는 로직 필요 + mailHistoryService.save(request) + return ResponseEntity.ok().build() + } + + @GetMapping("/{mailHistoryId}") + fun getById(@PathVariable mailHistoryId: Long): ResponseEntity> { + return ResponseEntity.ok(ApiResponse.success(mailHistoryService.getById(mailHistoryId))) + } + + @GetMapping + fun findAll(): ResponseEntity>> { + return ResponseEntity.ok(ApiResponse.success(mailHistoryService.findAll())) + } +} diff --git a/src/main/kotlin/support/domain/StringToListConverter.kt b/src/main/kotlin/support/domain/StringToListConverter.kt new file mode 100644 index 000000000..f375d0a1f --- /dev/null +++ b/src/main/kotlin/support/domain/StringToListConverter.kt @@ -0,0 +1,19 @@ +package support.domain + +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +@Converter +class StringToListConverter : AttributeConverter, String> { + override fun convertToDatabaseColumn(recipients: List): String { + return recipients.joinToString(COMMA) + } + + override fun convertToEntityAttribute(dbData: String): List { + return dbData.split(COMMA) + } + + companion object { + private const val COMMA: String = "," + } +} diff --git a/src/main/kotlin/support/views/Components.kt b/src/main/kotlin/support/views/Components.kt index 4f88ca059..0aaf596f6 100644 --- a/src/main/kotlin/support/views/Components.kt +++ b/src/main/kotlin/support/views/Components.kt @@ -64,7 +64,10 @@ private fun createBox( } textField.addKeyDownListener( Key.ENTER, - { eventListener(textField.value) } + { + eventListener(textField.value) + textField.clear() + } ) return HorizontalLayout( textField, diff --git a/src/main/resources/db/migration/V1_11__Add_email_history.sql b/src/main/resources/db/migration/V1_11__Add_email_history.sql new file mode 100644 index 000000000..bfb415459 --- /dev/null +++ b/src/main/resources/db/migration/V1_11__Add_email_history.sql @@ -0,0 +1,11 @@ +create table mail_history +( + id bigint not null auto_increment, + subject varchar(255) not null, + body longtext not null, + sender varchar(255) not null, + recipients longtext not null, + sent_time datetime(6) not null, + primary key (id) +) engine = InnoDB + default charset = utf8mb4; diff --git a/src/test/kotlin/apply/MailHistoryFixtures.kt.kt b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt new file mode 100644 index 000000000..d58835ab8 --- /dev/null +++ b/src/test/kotlin/apply/MailHistoryFixtures.kt.kt @@ -0,0 +1,33 @@ +package apply + +import apply.application.mail.MailData +import apply.domain.mail.MailHistory +import java.time.LocalDateTime + +private const val SUBJECT: String = "메일제목" +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() + +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, + sender: String = SENDER, + recipients: List = RECIPIENTS, + sentTime: LocalDateTime = SENT_TIME, + id: Long = 0L +): MailData { + return MailData(subject, body, sender, recipients, sentTime, id) +} diff --git a/src/test/kotlin/apply/application/MailHistoryServiceTest.kt b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt new file mode 100644 index 000000000..ebf976ae5 --- /dev/null +++ b/src/test/kotlin/apply/application/MailHistoryServiceTest.kt @@ -0,0 +1,48 @@ +package apply.application + +import apply.createMailData +import apply.createMailHistory +import apply.domain.mail.MailHistoryRepository +import apply.domain.user.UserRepository +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import support.test.UnitTest +import java.time.LocalDateTime + +@UnitTest +class MailHistoryServiceTest { + @MockK + private lateinit var mailHistoryRepository: MailHistoryRepository + + @MockK + private lateinit var userRepository: UserRepository + + private lateinit var mailHistoryService: MailHistoryService + + @BeforeEach + internal fun setUp() { + mailHistoryService = MailHistoryService(mailHistoryRepository, userRepository) + } + + @Test + fun `메일 이력을 저장한다`() { + val mailData = createMailData() + every { mailHistoryRepository.save(any()) } returns createMailHistory() + assertDoesNotThrow { mailHistoryService.save(mailData) } + } + + @Test + fun `저장된 메일 이력을 모두 조회한다`() { + val now = LocalDateTime.now() + val mailData1 = createMailData(subject = "제목1", sentTime = now) + val mailData2 = createMailData(subject = "제목2", sentTime = now.plusSeconds(1)) + val emailHistory1 = createMailHistory(subject = "제목1", sentTime = now) + val emailHistory2 = createMailHistory(subject = "제목2", sentTime = now.plusSeconds(1)) + every { mailHistoryRepository.findAll() } returns listOf(emailHistory1, emailHistory2) + assertThat(mailHistoryService.findAll()).containsExactly(mailData1, mailData2) + } +} diff --git a/src/test/kotlin/apply/application/MailTargetServiceTest.kt b/src/test/kotlin/apply/application/MailTargetServiceTest.kt index 873b7be3f..0c1a5da4b 100644 --- a/src/test/kotlin/apply/application/MailTargetServiceTest.kt +++ b/src/test/kotlin/apply/application/MailTargetServiceTest.kt @@ -11,6 +11,7 @@ import apply.domain.evaluationtarget.EvaluationStatus.PENDING import apply.domain.evaluationtarget.EvaluationStatus.WAITING import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.user.UserRepository +import apply.domain.user.findAllByEmailIn import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify @@ -109,4 +110,21 @@ class MailTargetServiceTest { assertThat(actual).hasSize(1) assertThat(actual[0].email).isEqualTo("waiting@email.com") } + + @Test + fun `이메일 중에서 회원에 해당하는 이메일이 있으면 (회원이름, 이메일)을, 회원에 해당하는 이메일이 없으면 (공백, 이메일)을 반환한다`() { + val users = listOf( + createUser(name = "회원1", email = "test1@email.com"), + createUser(name = "회원3", email = "test3@email.com") + ) + val emails = listOf("test1@email.com", "test2@email.com", "test3@email.com") + val mailTargetResponses = listOf( + MailTargetResponse("test1@email.com", "회원1"), + MailTargetResponse("test3@email.com", "회원3"), + MailTargetResponse("test2@email.com") + ) + every { userRepository.findAllByEmailIn(emails) } returns users + val actual = mailTargetService.findAllByEmails(emails) + assertThat(actual).isEqualTo(mailTargetResponses) + } } diff --git a/src/test/kotlin/apply/ui/api/EvaluationTargetRestControllerTest.kt b/src/test/kotlin/apply/ui/api/EvaluationTargetRestControllerTest.kt index 2951c609a..4d6fef9d0 100644 --- a/src/test/kotlin/apply/ui/api/EvaluationTargetRestControllerTest.kt +++ b/src/test/kotlin/apply/ui/api/EvaluationTargetRestControllerTest.kt @@ -249,7 +249,7 @@ internal class EvaluationTargetRestControllerTest : RestControllerTest() { evaluationId, enumStatus ) - } returns listOf(MailTargetResponse("김경록", "roki@woowacourse.com")) + } returns listOf(MailTargetResponse("roki@woowacourse.com", "김로키")) mockMvc.perform( RestDocumentationRequestBuilders.get( diff --git a/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt b/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt new file mode 100644 index 000000000..4bdd28143 --- /dev/null +++ b/src/test/kotlin/apply/ui/api/MailHistoryRestControllerTest.kt @@ -0,0 +1,67 @@ +package apply.ui.api + +import apply.application.MailHistoryService +import apply.application.UserService +import apply.application.mail.MailData +import apply.createMailData +import com.ninjasquad.springmockk.MockkBean +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import org.junit.jupiter.api.Test +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.FilterType +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post + +@WebMvcTest( + controllers = [MailHistoryRestController::class], + includeFilters = [ + ComponentScan.Filter(type = FilterType.REGEX, pattern = ["apply.security.*"]) + ] +) +class MailHistoryRestControllerTest : RestControllerTest() { + @MockkBean + private lateinit var userService: UserService + + @MockkBean + private lateinit var mailHistoryService: MailHistoryService + + @Test + fun `이메일 이력을 저장한다`() { + every { mailHistoryService.save(any()) } just Runs + + mockMvc.post("/api/mail-history") { + content = objectMapper.writeValueAsString(createMailData()) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { isOk } + } + } + + @Test + fun `이메일 내역을 단일 조회한다`() { + val mailData: MailData = createMailData() + every { mailHistoryService.getById(any()) } returns mailData + + mockMvc.get("/api/mail-history/{mailHistoryId}", mailData.id) + .andExpect { + status { isOk } + content { json(objectMapper.writeValueAsString(ApiResponse.success(mailData))) } + } + } + + @Test + fun `모든 이메일 내역을 조회한다`() { + val mailDataValues: List = listOf(createMailData(), createMailData()) + every { mailHistoryService.findAll() } returns mailDataValues + + mockMvc.get("/api/mail-history") + .andExpect { + status { isOk } + content { json(objectMapper.writeValueAsString(ApiResponse.success(mailDataValues))) } + } + } +}