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 the mail history function #377

Merged
merged 44 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8394d57
feat: 메일 이력관리 기능 기본 화면 구현
knae11 Oct 6, 2021
1e52c2a
feat: 메일 history 도메인 구현 및 화면 초안 구현
knae11 Oct 7, 2021
9f0f573
refactor: 이메일 이력 관리 서비스 분리 및 테스트 코드 작성
knae11 Oct 7, 2021
77f06ec
fixed: develop rebase 하며 충돌 난 부분 수정
knae11 Oct 7, 2021
684cdee
feat: 이메일 내역 상세조회 구현
knae11 Oct 7, 2021
b2d7682
refactor: mail로 이름변경 및 DB 데이터타입 수정
knae11 Oct 7, 2021
8fa97d8
feat: mailRestController 기능 및 테스트 구현
knae11 Oct 7, 2021
c9689e1
merge develop into feature/email-history
knae11 Oct 7, 2021
6ad823e
feat: 이메일 갯수를 보여주는 화면 구현
knae11 Oct 7, 2021
98c9070
feat: 이메일 갯수를 보여주는 화면 구현
knae11 Oct 7, 2021
04bd8d1
Merge branch 'develop' into feature/email-history
knae11 Oct 7, 2021
e05e892
merge develop into feature/email-history
knae11 Oct 8, 2021
8d3f706
Merge branch 'develop' into feature/email-history
knae11 Oct 8, 2021
9d99926
refactor: EmailHistoryRepository를 MailHistoryRepository로 변경
knae11 Oct 10, 2021
0e0ae51
refactor(UserService): 엘비스연산자로 수정
knae11 Oct 10, 2021
d3b095e
refactor: AttributeConverter 사용
knae11 Oct 10, 2021
10455b5
refactor: 지역변수 제거 및 생성자 활용
knae11 Oct 10, 2021
00cf954
refactor: recipientsCount 함수 프로퍼티에서 상태호출하도록 변경
knae11 Oct 10, 2021
bfdc527
refactor(view): 일부 값 공유 및 하드코딩으로 변경
knae11 Oct 10, 2021
7998efd
refactor: enterBox 입력후 textField 비워지게 수정
knae11 Oct 10, 2021
37b4db4
refactor: MailService -> MailSenderService, MailHistoryService -> Mai…
knae11 Oct 10, 2021
027ccf1
refactor: UserService에 있던 mail 관련 메소드 MailService로 이동
knae11 Oct 10, 2021
0a347b5
refactor(MailTargetResponse): NO_NAME 의존성 제거 및 email, name 순으로 생성자 인자…
knae11 Oct 10, 2021
825d03b
Merge branch 'develop' into feature/email-history
knae11 Oct 10, 2021
176808b
refactor: MailForm, MailFormView 분리
knae11 Oct 10, 2021
fc4b737
refactor: MailService, MailHistoryService 로 변경
knae11 Oct 10, 2021
41b00cb
refactor(test, view): 불필요한 타입 명시 제거, 실명 언급 수정
knae11 Oct 10, 2021
17d3966
refactor: 피드백 반영내용 기존 코드로 원복, 불필요한 데이터타입 삭제
knae11 Oct 11, 2021
64fec96
refactor: 글자수 제한 조건 설정
knae11 Oct 11, 2021
0cfbc29
refactor: 아마찌 피드백 반영
knae11 Oct 11, 2021
cc5f5a9
refactor(mailHistoryService): set 연산을 list 연산으로 수정
knae11 Oct 11, 2021
659bbe2
refactor: 나노초까지 나오지 않게 수정
knae11 Oct 11, 2021
9aaff7f
refactor: flyway 버전 수정
knae11 Oct 11, 2021
10aabf9
refactor: flyway 정렬
knae11 Oct 11, 2021
d2da60f
refactor: flyway 버전 11로 변경
knae11 Oct 12, 2021
4132714
refactor: View 복수형으로 이름 수정
knae11 Oct 13, 2021
431b2f3
Merge remote-tracking branch 'origin/develop' into feature/email-history
knae11 Oct 13, 2021
ac62a4e
refactor: view 관련 받는사람 부분 디테일 수정
knae11 Oct 13, 2021
2c8dd8e
refactor: 이름 없음의 기본 null로 설정하여 생성자 제거
knae11 Oct 13, 2021
41242e6
refactor: 이름 수정 및 mailTarget 메소드 서비스 위치 변경
knae11 Oct 13, 2021
5eab4d0
refactor(view): 받는사람 footer 로 변경
knae11 Oct 13, 2021
e970e6a
refactor(view): 불필요한 내용 삭제
knae11 Oct 13, 2021
6df55b3
style: polish the code
woowahan-pjs Oct 13, 2021
ea7dace
style: polish the code
woowahan-pjs Oct 13, 2021
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
9 changes: 6 additions & 3 deletions src/main/kotlin/apply/application/EvaluationDtos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -150,10 +151,12 @@ data class EvaluationTargetData(
)

data class MailTargetResponse(
val name: String,
val email: String
val email: String,
val name: String?
) {
constructor(userResponse: UserResponse) : this(userResponse.name, userResponse.email)
constructor(userResponse: UserResponse) : this(userResponse.email, userResponse.name)
constructor(user: User) : this(user.email, user.name)
constructor(email: String) : this(email, null)
}

data class EvaluationItemScoreData(
Expand Down
44 changes: 44 additions & 0 deletions src/main/kotlin/apply/application/MailHistoryService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 apply.domain.user.findAllByEmailIn
import org.springframework.stereotype.Service
import javax.transaction.Transactional

@Transactional
@Service
class MailHistoryService(
Copy link
Contributor

Choose a reason for hiding this comment

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

MailHistoryServiceMailService 를 따로 사용해주는군요!
개인적으로는 MailHistoryService 와 mailService 가 같이 사용되는 경우가 많을 것 같은데 분리하신 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저도 이 부분에 대해서 고민이 되었는데요. MailService@Transactional이 아닌 @Async로직을 사용하는 메소드로 구성되어 있더라구요. 같은 서비스에 넣을까하다가 비동기로직과 아닌 로직을 구분하는게 좋을 것 같아서 분리했어요.

혹시 다른 분들도 의견도 어떤지 궁금해요~ㅎㅎㅎ

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<MailData> {
return mailHistoryRepository.findAll().map { MailData(it) }
}

fun getById(mailHistoryId: Long): MailData {
val mailHistory = mailHistoryRepository.getById(mailHistoryId)
return MailData(mailHistory)
}

fun findAllMailTargetsByEmails(emails: List<String>): List<MailTargetResponse> {
val users = userRepository.findAllByEmailIn(emails)
val anonymousEmails = emails - users.map { it.email }
return users.map { MailTargetResponse(it) } + anonymousEmails.map { MailTargetResponse(it) }
}
Copy link
Contributor

@ecsimsw ecsimsw Oct 11, 2021

Choose a reason for hiding this comment

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

set이 더 효율적이라는 생각 일단 대박이네요. 저는 그렇게까지 생각 못했을 것 같아요.

추가로 확인해보니 list의 minus도 결국 set으로 변경하고 filterNot을 진행하네요?
이렇게 toSet 없이 List로 차집합을 구해도 성능상 손해는 없을 것 같아요.
한번 확인해주시고, 반영은 편하게 생각해주세요 :)

fun findAllMailTargetsByEmails(emails: List<String>): List<MailTargetResponse> {
    val users = userRepository.findAllByEmailIn(emails)
    val anonymousEmails = emails - users.map {it.email}
    return users.map { MailTargetResponse(it) } + anonymousEmails.map { MailTargetResponse(it) }
}

List의 minus 연산

public operator fun <T> Iterable<T>.minus(elements: Iterable<T>): List<T> {
    val other = elements.convertToSetForSetOperationWith(this)
    if (other.isEmpty())
        return this.toList()
    return this.filterNot { it in other }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오!! list 연산도 set으로 바꾸는지 몰랐는데 👍 👍 👍 list 방식으로 수정할게요!ㅎㅎㅎ

}
2 changes: 1 addition & 1 deletion src/main/kotlin/apply/application/MailTargetService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class MailTargetService(
fun findMailTargets(evaluationId: Long, evaluationStatus: EvaluationStatus? = null): List<MailTargetResponse> {
val userIds = findEvaluationTargets(evaluationId, evaluationStatus).map { it.userId }
return userRepository.findAllById(userIds)
.map { MailTargetResponse(it.name, it.email) }
.map { MailTargetResponse(it.email, it.name) }
}

private fun findEvaluationTargets(evaluationId: Long, evaluationStatus: EvaluationStatus?): List<EvaluationTarget> {
Expand Down
29 changes: 27 additions & 2 deletions src/main/kotlin/apply/application/mail/MailData.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
package apply.application.mail

import apply.domain.mail.MailHistory
import support.createLocalDateTime
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<String> = emptyList()
)
@field:Size(min = 1, max = 100)
var sender: String = "",
Copy link
Contributor

Choose a reason for hiding this comment

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

갑자기 뜬금없는 얘기지만 주로 DTOxxxData로 명명하는 data 클래스는 각각 필드를 전자는 val, 후자는 var로 표기하는데 이유가 있을까요? 🤔

(배럴뿐만 아니라 다른 크루들 중에서도 아시는 분 있으면 좀 알려주세요 🥲)


@field:NotEmpty
var recipients: List<String> = emptyList(),

@field:NotNull
var sentTime: LocalDateTime = createLocalDateTime(LocalDateTime.now()),

@field:NotNull
var id: Long = 0L
) {
constructor(mailHistory: MailHistory) : this(
mailHistory.subject,
mailHistory.body,
mailHistory.sender,
mailHistory.recipients,
mailHistory.sentTime,
mailHistory.id
)
}
31 changes: 31 additions & 0 deletions src/main/kotlin/apply/domain/mail/MailHistory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package apply.domain.mail

import support.domain.BaseEntity
import support.domain.RecipientsConverter
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

개인적인 생각이지만 subject랑 sender는 길이 제한을 두는게 어떨까요? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

View 단에서 제한을 두는게 좋다는 의견일까요??? 지금 DB에 값으로 들어갈 때는 varchar(255)로 들어가고 있는데 혹시 이 부분을 명시하는게 좋다는 의견일까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

뷰에서 제한을 두면 좋겠다는 의미일 것 같은데 혹시 제한을 한다면 DB 내부 타입과 같이 @field:Size(min = 1, max = 255)로 하는게 좋을까요? 혹은 더 적은 100자 정도로 하는게 좋을까요? 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

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

한글타입이라 byte 수가 다를 수 있어서 title 과 sender 둘 다 100자면 충분할 것 같아 100자로 제한하였습니다. JPA에서 @Column String 타입에서 varchar(255) 로 설정되어 있는 것으로 아는데 혹시 이 부분도 명시하고 싶은 의견인가요?

Copy link
Contributor

Choose a reason for hiding this comment

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

제 의견은 Entity에도 제약조건을 두는 것이 좋다고 생각했는데 다른 Entity 중에서 따로 길이제한을 두는 Entity가 없어서 거기까지는 안하셔도 될 것 같아요 😃

val subject: String,

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

@Column(nullable = false)
val sender: String,

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

@Column(nullable = false)
val sentTime: LocalDateTime = LocalDateTime.now(),
id: Long = 0L
) : BaseEntity(id)
8 changes: 8 additions & 0 deletions src/main/kotlin/apply/domain/mail/MailHistoryRepository.kt
Original file line number Diff line number Diff line change
@@ -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<MailHistory, Long>
4 changes: 2 additions & 2 deletions src/main/kotlin/apply/ui/admin/BaseLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.MailView
import apply.ui.admin.recruitment.RecruitmentsView
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.applayout.AppLayout
Expand Down Expand Up @@ -58,7 +58,7 @@ class BaseLayout(
"과제 관리".accordionOf("admin/missions", recruitments),
"선발 과정".accordionOf("admin/selections", recruitments),
"부정 행위자" of CheatersView::class.java,
"메일 발송" of MailFormView::class.java
"메일 관리" of MailView::class.java
)
}
}
3 changes: 2 additions & 1 deletion src/main/kotlin/apply/ui/admin/mail/GroupMailTargetDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,7 +40,7 @@ class GroupMailTargetDialog(

private fun createMailTargetsGrid(): Grid<MailTargetResponse> {
return Grid<MailTargetResponse>(10).apply {
addSortableColumn("이름", MailTargetResponse::name)
addSortableColumn("이름") { it.name ?: NO_NAME }
addSortableColumn("이메일", MailTargetResponse::email)
}
}
Expand Down
175 changes: 175 additions & 0 deletions src/main/kotlin/apply/ui/admin/mail/MailForm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package apply.ui.admin.mail

import apply.application.EvaluationService
import apply.application.MailHistoryService
import apply.application.MailTargetResponse
import apply.application.MailTargetService
import apply.application.RecruitmentService
import apply.application.UserService
import apply.application.mail.MailData
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.grid.Grid
import com.vaadin.flow.component.html.Label
import com.vaadin.flow.component.orderedlayout.FlexComponent
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
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 org.springframework.boot.autoconfigure.mail.MailProperties
import support.views.BindingFormLayout
import support.views.NO_NAME
import support.views.addSortableColumn
import support.views.createErrorSmallButton
import support.views.createNormalButton
import support.views.createUpload

class MailForm(
private val userService: UserService,
private val recruitmentService: RecruitmentService,
private val evaluationService: EvaluationService,
private val mailTargetService: MailTargetService,
private val mailHistoryService: MailHistoryService,
private val mailProperties: MailProperties
) : BindingFormLayout<MailData>(MailData::class) {
private val subject: TextField = TextField("제목").apply { setWidthFull() }
private val body: TextArea = createBody()
private val mailTargets: MutableSet<MailTargetResponse> = mutableSetOf()
private val mailTargetsGrid: Grid<MailTargetResponse> = createMailTargetsGrid(mailTargets)
private val mailTargetGridTitle: Label = Label()
Copy link
Contributor

Choose a reason for hiding this comment

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

라벨이 footer로 바뀌면서 더이상 필요 없는것 같네요~

Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 내용 삭제했어요! 👍 :)

private val recipientFilter: Component = createRecipientFilter()
private val fileUpload: Component = createFileUpload()

init {
add(subject, createSender(), recipientFilter, mailTargetGridTitle, mailTargetsGrid, body, fileUpload)
setResponsiveSteps(ResponsiveStep("0", 1))
drawRequired()
}

fun toReadOnlyMode() {
this.subject.isReadOnly = true
this.body.isReadOnly = true
this.recipientFilter.isVisible = false
this.mailTargetsGrid.getColumnByKey(DELETE_BUTTON).isVisible = false
this.fileUpload.isVisible = false
}

private fun createSender(): Component {
return TextField("보낸사람").apply {
value = mailProperties.username
isReadOnly = true
}
}

private fun createRecipientFilter(): Component {
return HorizontalLayout(
createEnterBox(),
createIndividualLoadButton(),
createGroupLoadButton()
).apply { defaultVerticalComponentAlignment = FlexComponent.Alignment.END }
}

private fun createEnterBox(): Component {
return support.views.createEnterBox(labelText = "받는사람 추가") {
if (it.isNotBlank()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

이메일 형식이 아닌경우에도 입력이 안되게 예외처리(= 입력X + 에러창)하면 어떨까요? 🤔

이메일 정규식: [0-9a-zA-Z]([-_.]?[0-9a-zA-Z][(-_.]?[0-9a-zA-Z]{2,3} 🤗

(관리자는 실수하지 않으니까... 스킵하셔두 될지도 ...? 🙄)

mailTargets.addAndRefresh(MailTargetResponse(it, NO_NAME))
}
}
}

private fun createIndividualLoadButton(): Button {
return createNormalButton("개별 불러오기") {
IndividualMailTargetDialog(userService) {
mailTargets.addAndRefresh(it)
}
}
}

private fun createGroupLoadButton(): Component {
return createNormalButton("그룹 불러오기") {
GroupMailTargetDialog(recruitmentService, evaluationService, mailTargetService) {
mailTargets.addAllAndRefresh(it)
}
}
}

private fun setRowCount(count: Int) {
mailTargetGridTitle.text = "받는사람 ($count)"
}

private fun createMailTargetsGrid(mailTargets: Set<MailTargetResponse>): Grid<MailTargetResponse> {
return Grid<MailTargetResponse>(10).apply {
addSortableColumn("이름") { it.name ?: NO_NAME }
addSortableColumn("이메일", MailTargetResponse::email)
addColumn(createRemoveButton()).key = DELETE_BUTTON
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
addColumn(createRemoveButton()).key = DELETE_BUTTON
addColumn(createRemoveButton()).apply { key = DELETE_BUTTON }

동작은 같겠지만 여기는 apply를 적용해서 column을 만드는 컨텍스트에 key 설정을 추가하는 느낌을 주면 어떨까요?

setItems(mailTargets)
}
Comment on lines +94 to +95
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
setItems(mailTargets)
}
setItems(mailTargets)
dataProvider.addDataProviderListener {
columns.first().setFooter("받는사람: ${mailTargets.size}")
}
}

이렇게 사용하면 refreshRowCount()가 필요없긴 하네요.
전 직관적인 걸 좋아해서 지금 코드가 더 좋긴합니다!

}

private fun createBody(): TextArea {
return TextArea("본문").apply {
setSizeFull()
style.set("minHeight", "400px")
}
}

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 createRemoveButton(): Renderer<MailTargetResponse> {
return ComponentRenderer<Component, MailTargetResponse> { response ->
createErrorSmallButton("제거") {
mailTargets.removeAndRefresh(response)
}
}
}

private fun MutableSet<MailTargetResponse>.addAndRefresh(element: MailTargetResponse) {
add(element).also {
mailTargetsGrid.dataProvider.refreshAll()
Copy link
Contributor

Choose a reason for hiding this comment

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

이 친구도 함수로 묶어주면 좋을 거 같아요. 여러군데서 반복해서 사용하고 있네요

setRowCount(mailTargets.size)
}
}

private fun MutableSet<MailTargetResponse>.addAllAndRefresh(elements: Collection<MailTargetResponse>) {
addAll(elements).also {
mailTargetsGrid.dataProvider.refreshAll()
setRowCount(mailTargets.size)
}
}

private fun MutableSet<MailTargetResponse>.removeAndRefresh(element: MailTargetResponse) {
remove(element).also {
mailTargetsGrid.dataProvider.refreshAll()
setRowCount(mailTargets.size)
}
}

override fun bindOrNull(): MailData? {
return bindDefaultOrNull()?.apply {
recipients = mailTargets.map { it.email }.toList()
}
}

override fun fill(data: MailData) {
setRowCount(data.recipients.size)
fillDefault(data)
mailTargets.addAll(mailHistoryService.findAllMailTargetsByEmails(data.recipients))
}

companion object {
private const val DELETE_BUTTON: String = "삭제버튼"
}
}
Loading