-
Notifications
You must be signed in to change notification settings - Fork 102
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
Changes from 33 commits
8394d57
1e52c2a
9f0f573
77f06ec
684cdee
b2d7682
8fa97d8
c9689e1
6ad823e
98c9070
04bd8d1
e05e892
8d3f706
9d99926
0e0ae51
d3b095e
10455b5
00cf954
bfdc527
7998efd
37b4db4
027ccf1
0a347b5
825d03b
176808b
fc4b737
41b00cb
17d3966
64fec96
0cfbc29
cc5f5a9
659bbe2
9aaff7f
10aabf9
d2da60f
4132714
431b2f3
ac62a4e
2c8dd8e
41242e6
5eab4d0
e970e6a
6df55b3
ea7dace
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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( | ||
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) } | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. set이 더 효율적이라는 생각 일단 대박이네요. 저는 그렇게까지 생각 못했을 것 같아요. 추가로 확인해보니 list의 minus도 결국 set으로 변경하고 filterNot을 진행하네요? 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 }
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오!! list 연산도 set으로 바꾸는지 몰랐는데 👍 👍 👍 list 방식으로 수정할게요!ㅎㅎㅎ |
||
} |
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 = "", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 갑자기 뜬금없는 얘기지만 주로 (배럴뿐만 아니라 다른 크루들 중에서도 아시는 분 있으면 좀 알려주세요 🥲) |
||
|
||
@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 | ||
) | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 개인적인 생각이지만 subject랑 sender는 길이 제한을 두는게 어떨까요? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. View 단에서 제한을 두는게 좋다는 의견일까요??? 지금 DB에 값으로 들어갈 때는 varchar(255)로 들어가고 있는데 혹시 이 부분을 명시하는게 좋다는 의견일까요?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뷰에서 제한을 두면 좋겠다는 의미일 것 같은데 혹시 제한을 한다면 DB 내부 타입과 같이 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 한글타입이라 byte 수가 다를 수 있어서 title 과 sender 둘 다 100자면 충분할 것 같아 100자로 제한하였습니다. JPA에서 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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> |
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() | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 라벨이 footer로 바뀌면서 더이상 필요 없는것 같네요~ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) { | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이메일 형식이 아닌경우에도 입력이 안되게 예외처리(= 입력X + 에러창)하면 어떨까요? 🤔 이메일 정규식: (관리자는 실수하지 않으니까... 스킵하셔두 될지도 ...? 🙄) |
||||||||||||||||
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 | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
동작은 같겠지만 여기는 |
||||||||||||||||
setItems(mailTargets) | ||||||||||||||||
} | ||||||||||||||||
Comment on lines
+94
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
이렇게 사용하면 |
||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
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() | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = "삭제버튼" | ||||||||||||||||
} | ||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MailHistoryService
와MailService
를 따로 사용해주는군요!개인적으로는 MailHistoryService 와 mailService 가 같이 사용되는 경우가 많을 것 같은데 분리하신 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 이 부분에 대해서 고민이 되었는데요.
MailService
는@Transactional
이 아닌@Async
로직을 사용하는 메소드로 구성되어 있더라구요. 같은 서비스에 넣을까하다가 비동기로직과 아닌 로직을 구분하는게 좋을 것 같아서 분리했어요.혹시 다른 분들도 의견도 어떤지 궁금해요~ㅎㅎㅎ