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 send mail view #337

Merged
merged 23 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
393d5f0
feat: 메일 발송 페이지 기능 베이스 레이아웃 추가
Rok93 Sep 13, 2021
947ac76
feat: 대략적인 개별, 그룹 발송 기능 틀 구현
Rok93 Sep 15, 2021
cf2f20f
feat: 업로드 기능 제외한 메일 발송 페이지 구현
Rok93 Sep 15, 2021
b528570
feat: 업로드 버튼 추가
Rok93 Sep 16, 2021
c4b8f59
refactor: 메일 첨부파일 업로드 기능의 최대 용량을 10MB로 변경 및 모든 파일 타입을 업로드 허용하도록 변경
Rok93 Sep 20, 2021
8e6b68f
refactor: 첨부파일 업로드 버튼이 메일 본문 아래에 위치하도록 변경
Rok93 Sep 20, 2021
e6322b0
feat: 메일을 보내는 발신자 표시 추가
Rok93 Sep 20, 2021
47231b0
refactor: 개별 발송, 그룹 발송 기능을 하나의 페이지에서 가능하도록 변경
Rok93 Sep 21, 2021
d9f5c93
refactor: 그룹 발송 기능을 Dialog 컴포넌트 클래스로 분리
Rok93 Sep 22, 2021
24f387e
refactor: 개별 발송 기능을 Dialog 컴포넌트 클래스로 분리
Rok93 Sep 22, 2021
355547b
refactor: 개별 발송 Dialog 컴포넌트 클래스의 파라미터 순서 변경
Rok93 Sep 22, 2021
cc54a2d
refactor: 개별 발송 Dialog의 추가 버튼을 적용 버튼으로 변경
Rok93 Sep 22, 2021
c7fc6c4
resolve conflict
Rok93 Sep 23, 2021
cd0c86e
feat: form layout 적용 및 Grid 추가
Rok93 Sep 23, 2021
619dfde
feat: MailTarget Grid 삭제 버튼 추가
Rok93 Sep 23, 2021
e22c80a
feat: MailTarget Grid 삭제 버튼 추가
Rok93 Sep 23, 2021
a5a7bda
refactor: 메일의 각 컴포넌트의 라벨을 텍스트 라벨로 변경
Rok93 Sep 24, 2021
7bbad19
refactor: UI 피드백 반영
Rok93 Sep 25, 2021
7ab7abb
Merge branch 'develop' into feature/send-mail-page
Rok93 Sep 29, 2021
7566eb1
refactor: 마찌 피드백 반영
Rok93 Oct 1, 2021
97a7bd4
refactor(mail): polish the code
woowahan-pjs Oct 4, 2021
32f321b
refactor(support): rename search bar to search box
woowahan-pjs Oct 4, 2021
98597dd
refactor(mail): polish the code
woowahan-pjs Oct 4, 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
4 changes: 3 additions & 1 deletion src/main/kotlin/apply/ui/admin/BaseLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apply.ui.admin

import apply.ui.admin.cheater.CheatersView
import apply.ui.admin.evaluation.EvaluationsView
import apply.ui.admin.mail.MailSelectionsView
import apply.ui.admin.recruitment.RecruitmentsView
import apply.ui.admin.selections.SelectionsView
import com.vaadin.flow.component.Component
Expand All @@ -25,7 +26,8 @@ class BaseLayout : AppLayout() {
"모집 관리" to RecruitmentsView::class.java,
"평가 관리" to EvaluationsView::class.java,
"선발 과정" to SelectionsView::class.java,
"부정 행위자" to CheatersView::class.java
"부정 행위자" to CheatersView::class.java,
"메일 발송" to MailSelectionsView::class.java
)

init {
Expand Down
75 changes: 75 additions & 0 deletions src/main/kotlin/apply/ui/admin/mail/GroupMailFormView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package apply.ui.admin.mail

import apply.application.EvaluationService
import apply.application.MailTargetService
import apply.application.RecruitmentResponse
import apply.application.RecruitmentService
import apply.domain.evaluation.Evaluation
import apply.domain.evaluationtarget.EvaluationStatus
import apply.ui.admin.BaseLayout
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
import com.vaadin.flow.component.select.Select
import com.vaadin.flow.router.Route
import support.views.Title
import support.views.createItemSelect

@Route(value = "group", layout = BaseLayout::class)
class GroupMailFormView(
private val recruitmentService: RecruitmentService,
private val evaluationService: EvaluationService,
private val mailTargetService: MailTargetService
) : MailFormView() {
private val recruitment: Select<RecruitmentResponse>
private val evaluation: Select<Evaluation>
private val evaluationStatus: Select<EvaluationStatus>

init {
evaluation = createEvaluationItem()
evaluationStatus = createEvaluationStatusItem(evaluation)
recruitment = createRecruitmentItem(evaluation)
add(Title("그룹 발송"), createMailForm())
setWidthFull()
}

override fun createRecipientFilter(): Component {
return HorizontalLayout(recruitment, evaluation, evaluationStatus)
}

private fun createRecruitmentItem(evaluation: Select<Evaluation>): Select<RecruitmentResponse> {
return createItemSelect<RecruitmentResponse>("모집").apply {
setItems(*recruitmentService.findAll().toTypedArray())
setItemLabelGenerator { it.title }
addValueChangeListener {
evaluation.apply {
setItems(*evaluationService.findAllByRecruitmentId(it.value.id).toTypedArray())
setItemLabelGenerator { it.title }
}
}
}
}

private fun createEvaluationItem(): Select<Evaluation> {
return createItemSelect("평가")
}

private fun createEvaluationStatusItem(evaluation: Select<Evaluation>): Select<EvaluationStatus> {
return createItemSelect<EvaluationStatus>("모집 상태").apply {
setItems(*EvaluationStatus.values())
setItemLabelGenerator { it.toText() }
addValueChangeListener {
clearCurrentRecipients()
val mailTargets = mailTargetService.findMailTargets(evaluation.value.id, it.value).map { it.email }
mailTargets.forEach { addRecipientComponent(it) }
}
}
}

private fun EvaluationStatus.toText() =
when (this) {
EvaluationStatus.WAITING -> "평가 전"
EvaluationStatus.PASS -> "합격"
EvaluationStatus.FAIL -> "탈락"
EvaluationStatus.PENDING -> "보류"
}
}
153 changes: 153 additions & 0 deletions src/main/kotlin/apply/ui/admin/mail/IndividualMailFormView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package apply.ui.admin.mail

import apply.application.ApplicantResponse
import apply.application.ApplicantService
import apply.ui.admin.BaseLayout
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.Key
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.dialog.Dialog
import com.vaadin.flow.component.grid.Grid
import com.vaadin.flow.component.html.Div
import com.vaadin.flow.component.html.H2
import com.vaadin.flow.component.icon.Icon
import com.vaadin.flow.component.icon.VaadinIcon
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.TextField
import com.vaadin.flow.data.renderer.ComponentRenderer
import com.vaadin.flow.data.renderer.Renderer
import com.vaadin.flow.router.Route
import support.views.Title
import support.views.addSortableColumn
import support.views.createErrorButton
import support.views.createNormalButton
import support.views.createPrimaryButton
import support.views.createSearchBar

@Route(value = "personal", layout = BaseLayout::class)
class IndividualMailFormView(
private val applicantService: ApplicantService
) : MailFormView() {
init {
add(Title("개별 발송"), createMailForm())
setWidthFull()
}

override fun createRecipientFilter(): Component {
return HorizontalLayout(
createDirectInsertTargetComponent {
if (it.isNotBlank()) {
addRecipientComponent(it)
}
},
createSearchTargetComponent()
)
}

private fun createDirectInsertTargetComponent(eventListener: (name: String) -> Unit): Div {
val mailTarget = TextField().apply {
addKeyDownListener(
Key.ENTER,
{
eventListener(this.value)
this.value = ""
}
)
}

return Div(
HorizontalLayout(
mailTarget,
Button(Icon(VaadinIcon.ENTER_ARROW)) {
eventListener(mailTarget.value)
mailTarget.value = ""
}
)
)
}

private fun createSearchTargetComponent(): Button {
return createNormalButton("지원자 조회") {
Dialog().apply {
width = "800px"
height = "90%"
add(
H2("지원자 정보 검색"),
HorizontalLayout(
createAddRecipients(),
HorizontalLayout(
createErrorButton("취소") {
close()
}
).apply {
justifyContentMode = FlexComponent.JustifyContentMode.END
defaultHorizontalComponentAlignment = FlexComponent.Alignment.END
}
)
).apply {
setWidthFull()
}
open()
}
}.apply { isEnabled = true }
}

private fun createAddRecipients(): Component {
val container = VerticalLayout()
return VerticalLayout(
createSearchBar {
container.removeAll()
val founds = applicantService.findAllByKeyword(it)
if (founds.isNotEmpty()) {
container.add(
createGrid(founds)
)
}
},
container
)
}

private fun createGrid(applicantResponse: List<ApplicantResponse>): Component {
return Grid<ApplicantResponse>(10).apply {
addSortableColumn("이름", ApplicantResponse::name)
addSortableColumn("이메일", ApplicantResponse::email)
addColumn(createEditAndDeleteButton()).apply { isAutoWidth = true }
setItems(applicantResponse)
}
}

private fun createEditAndDeleteButton(): Renderer<ApplicantResponse> {
return ComponentRenderer<Component, ApplicantResponse> { applicantResponse ->
HorizontalLayout(
createAddOrDeleteButton(applicantResponse)
)
}
}

private fun createAddOrDeleteButton(applicantResponse: ApplicantResponse): Component {
if (this.recipients.contains(applicantResponse.email)) {
return createTargetDeleteButton(applicantResponse)
}

return createTargetAddButton(applicantResponse)
}

private fun createTargetAddButton(applicantResponse: ApplicantResponse): Button {
return createPrimaryButton("추가") {
addRecipientComponent(applicantResponse.email)
}.apply {
isDisableOnClick = true
}
}

private fun createTargetDeleteButton(applicantResponse: ApplicantResponse): Button {
return createErrorButton("삭제") {
removeRecipientComponent(applicantResponse.email)
}.apply {
isDisableOnClick = true
}
}
}
133 changes: 133 additions & 0 deletions src/main/kotlin/apply/ui/admin/mail/MailFormView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package apply.ui.admin.mail

import com.vaadin.flow.component.Component
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.html.H3
import com.vaadin.flow.component.html.H4
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.icon.Icon
import com.vaadin.flow.component.icon.VaadinIcon
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.receivers.MultiFileMemoryBuffer
import com.vaadin.flow.router.RoutePrefix
import support.views.createImageUpload
import support.views.createPrimaryButton

@RoutePrefix(value = "admin/emails")
abstract class MailFormView() : VerticalLayout() {
protected val subject: TextField = TextField("메일 제목", "메일 제목 입력")
protected val recipients: MutableList<String> = mutableListOf()
protected val body: TextArea = createMailBody()
private val currentRecipients = HorizontalLayout()
Copy link
Contributor

Choose a reason for hiding this comment

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

image
ㅠㅠㅠㅠ 저 바딘 잘 몰라서 사실 잘 모르는데... 갯수가 많아지면 옆으로 계속 늘어날 것 같은데 이건 괜찮까 싶네요!
단체메일보내면 꽤 많을 듯 싶어서요.
더불어..x의 위치도 아래로 변경됩니당!

Copy link
Contributor

Choose a reason for hiding this comment

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

개별 조회에서 지원자 조회 탭을 누르지 않고 test 하고 엔터만 치면 그대로 입력되는데 이것도 괜찮을까요..? 해당하는 이메일을 불러와야 하지 않을까 싶어서요 혹은 엔터 칸을 없애야하지 않을까 싶기도 해요! 로키의 작업범위가 아니라면 알려주세요~ㅎㅎㅎ

Copy link
Contributor Author

@Rok93 Rok93 Sep 22, 2021

Choose a reason for hiding this comment

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

엔터 키를 누르거나 혹은 엔터 버튼을 눌렀을 때, 직접 작성한 이메일이 등록되는 기능은 이슈의 내용 중 일부인 아래의 요구사항 때문에 추가한 기능입니다.
현재 저희 지원 페이지에 가입되어있는 이메일이 아니더라도 메일을 보낼 수 있도록하기 위함입니다 😃
(물론 이메일양식에 맞지 않는 이메일을 등록한다거나... 등등 여러가지 예외 사항들을 잡으면 좋겠지만... 그 부분까지는 고려하지 못했네요 🥲)

요구사항 중 일부

  • 메일 주소를 직접 입력하여 특정한다.
    • 테스트 등을 위해 직접 메일주소를 입력하여 발송할 수 있어야 한다.
    • 발송할 메일 주소 입력 UI를 구현한다.

Copy link
Contributor Author

@Rok93 Rok93 Sep 22, 2021

Choose a reason for hiding this comment

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

배럴이 발견하신 문제점... x위치가 아래로 내려가는 문제는 currentRecipients를 Div 컴포넌트로 변경하니까 전보다는 나아진 것 같네요 🤔

변경 후 모습
image


abstract fun createRecipientFilter(): Component

protected fun createMailForm(): Component {
val subjectText = VerticalLayout(
H3("메일 제목"),
subject.apply { setSizeFull() }
)

val recipientFilter = VerticalLayout(H4("수신자"), createRecipientFilter())
val uploadFile = createImageUpload("첨부파일", MultiFileMemoryBuffer()) {
/*
todo: 추후 업로드 된 파일을 메일로 첨부하는 로직이 추가되어야 함
(uploadFiles 같은 필드를 두고 mail을 보내는 기능에 포함시키면 될 것 같음)
it.files.forEach { fileName ->
val fileData = it.getFileData(fileName)
val inputStream = it.getInputStream(fileName)
val readBytes = inputStream.readBytes()
}
*/
}.apply {
setWidthFull()
}

val mailBody = VerticalLayout(
body
).apply {
setSizeFull()
}

val sendButton = createMailSendButton()

return VerticalLayout(
subjectText,
recipientFilter,
currentRecipients.apply { setSizeFull() },
uploadFile,
mailBody,
Copy link
Contributor

Choose a reason for hiding this comment

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

VerticalLayout(currentRecipients.apply { setSizeFull() }),
VerticalLayout(uploadFile),

기존

image

수정후

image

이렇게 해주면 해당 라인이 맞아져요.ㅎㅎㅎ 이거 VerticalLayout이 들어가게 메소드로 빼서 예쁘게 코드 수정해 주셔도 좋을 듯 싶어요! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재 대대적인 개편작업 중인데, 때마침 이 부분은 배럴이 코멘트 남겨주신 것과 동일하게 변경했네요! 👍

sendButton
).apply {
setSizeFull()
justifyContentMode = FlexComponent.JustifyContentMode.CENTER
defaultHorizontalComponentAlignment = FlexComponent.Alignment.CENTER
}
}

private fun createMailSendButton(): VerticalLayout {
return VerticalLayout(
createPrimaryButton("전송") {
// todo: emailService.메일전송(subject.value, recipients, body.value)
// MailService에
}
).apply {
setSizeFull()
justifyContentMode = FlexComponent.JustifyContentMode.CENTER
defaultHorizontalComponentAlignment = FlexComponent.Alignment.CENTER
}
}

protected fun addRecipientComponent(email: String) {
this.currentRecipients.apply {
recipients.add(email)
add(createRecipientComponent(email))
}
}

protected fun clearCurrentRecipients() {
this.currentRecipients.removeAll()
this.recipients.clear()
}

protected fun removeRecipientComponent(email: String) {
recipients.remove(email)
addAllCurrentRecipientComponent()
}

private fun addAllCurrentRecipientComponent() {
this.currentRecipients.removeAll()
this.currentRecipients.apply {
recipients.forEach {
add(createRecipientComponent(it))
Copy link
Contributor

Choose a reason for hiding this comment

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

image
여러개를 추가할 때, 추가되어도 옅은 색으로만 바뀌어서 좀 헷갈리는 것 같아요. 추가가 되면 추가 -> 삭제로 변경되거나 색 대비를 더 주어도 좋을 것 같다는 생각이 들어요! 혹시 어려운 작업이라면 반영하지 않으셔도 좋습니다!ㅎㅎㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이 부분은 저도 변경해보고 싶었는데... component를 replace 하도록 해보려했는데... 생각한대로 잘 안돼서 지금처럼 Disable로 변경했는데... 음 일단은 지금 방식이 최선이었는데 이 부분은 혹시 추가 버튼 클릭 시에, 삭제 버튼(다른 버튼)으로 변경할 수 있는 방법 아시는 분...의 제보를 받습니다 ... 썸바딘 헬프 유...

}
}
}

private fun createMailBody(): TextArea {
return TextArea("메일 본문").apply {
setSizeFull()
style.set("minHeight", "400px")
placeholder = "메일 본문 입력"
}
}

private fun createRecipientComponent(email: String): Component {
val emailTarget = TextField().apply {
value = email
isReadOnly = true
style.set("background-color", "#00B493")
Copy link
Contributor

Choose a reason for hiding this comment

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

혹..시 초록색 배경은 어떤 이유인가요?ㅎㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아.... 배민 색을 따온건데.... 👀
다들 이 색상에 대한 궁금증을 가지시더라구요 😅
무난한 회색으로 변경하였습니다. 😃

}

return Span(
emailTarget,
Button(Icon(VaadinIcon.CLOSE_SMALL)) {
removeRecipientComponent(email)
}
)
}
}
Loading