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

[Refactor/#351] WorkBookSubscriberWriter 책임 분리 리펙토링 #352

Merged
merged 7 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,241 +1,113 @@
package com.few.batch.service.article.writer

import com.few.batch.data.common.code.BatchCategoryType
import com.few.batch.data.common.code.BatchMemberType
import com.few.batch.service.article.dto.WorkBookSubscriberItem
import com.few.batch.service.article.dto.toMemberIds
import com.few.batch.service.article.dto.toTargetWorkBookIds
import com.few.batch.service.article.dto.toTargetWorkBookProgress
import com.few.batch.service.article.writer.model.*
import com.few.batch.service.article.writer.service.*
import com.few.batch.service.article.writer.support.MailSendRecorder
import com.few.batch.service.article.writer.support.MailServiceArgsGenerator
import com.few.email.service.article.SendArticleEmailService
import com.few.email.service.article.dto.Content
import com.few.email.service.article.dto.SendArticleEmailArgs
import jooq.jooq_dsl.tables.*
import org.jooq.DSLContext
import org.jooq.UpdateConditionStep
import org.jooq.impl.DSL
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.net.URL
import java.time.LocalDate
import java.time.LocalDateTime

data class MemberReceiveArticle(
val workbookId: Long,
val articleId: Long,
val dayCol: Long,
)

data class MemberReceiveArticles(
val articles: List<MemberReceiveArticle>,
) {
fun getByWorkBookIdAndDayCol(workbookId: Long, dayCol: Long): MemberReceiveArticle {
return articles.find {
it.workbookId == workbookId && it.dayCol == dayCol
} ?: throw IllegalArgumentException("Cannot find article by workbookId: $workbookId, dayCol: $dayCol")
}

fun getArticleIds(): List<Long> {
return articles.map {
it.articleId
}
}
}

data class ArticleContent(
val id: Long,
val category: String, // todo fix
val articleTitle: String,
val articleContent: String,
val writerName: String,
val writerLink: URL,
)

data class ReceiveLastDayMember(
val memberId: Long,
val targetWorkBookId: Long,
)

fun List<ArticleContent>.peek(articleId: Long): ArticleContent {
return this.find {
it.id == articleId
} ?: throw IllegalArgumentException("Cannot find article by articleId: $articleId")
}

@Component
class WorkBookSubscriberWriter(
private val dslContext: DSLContext,

private val browseMemberEmailService: BrowseMemberEmailService,
private val browseMemberReceiveArticlesService: BrowseMemberReceiveArticlesService,
private val browseArticleContentsService: BrowseArticleContentsService,
private val browseWorkbookLastDayColService: BrowseWorkbookLastDayColService,

private val sendArticleEmailService: SendArticleEmailService,
) {

companion object {
private const val ARTICLE_SUBJECT_TEMPLATE = "Day%d %s"
private const val ARTICLE_TEMPLATE = "article"
}

/**
* 구독자들에게 이메일을 전송하고 진행률을 업데이트한다.
*/
@Transactional
fun execute(items: List<WorkBookSubscriberItem>): Map<Any, Any> {
val memberT = Member.MEMBER
val mappingWorkbookArticleT = MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE
val articleIfoT = ArticleIfo.ARTICLE_IFO
val articleMstT = ArticleMst.ARTICLE_MST
val subscriptionT = Subscription.SUBSCRIPTION

val memberIds = items.toMemberIds()
val targetWorkBookIds = items.toTargetWorkBookIds()
val targetWorkBookProgress = items.toTargetWorkBookProgress()

/** 회원 ID를 기준으로 이메일을 조회한다.*/
val memberEmailRecords = dslContext.select(
memberT.ID,
memberT.EMAIL
)
.from(memberT)
.where(memberT.ID.`in`(memberIds))
.fetch()
.intoMap(memberT.ID, memberT.EMAIL)

/** 구독자들이 구독한 학습지 ID와 구독자의 학습지 구독 진행률을 기준으로 구독자가 받을 학습지 정보를 조회한다.*/
val memberReceiveArticles = targetWorkBookProgress.keys.stream().map { workbookId ->
val dayCols = targetWorkBookProgress[workbookId]!!.stream().map { it + 1L }.toList()
// todo check refactoring
dslContext.select(
mappingWorkbookArticleT.WORKBOOK_ID.`as`(MemberReceiveArticle::workbookId.name),
mappingWorkbookArticleT.ARTICLE_ID.`as`(MemberReceiveArticle::articleId.name),
mappingWorkbookArticleT.DAY_COL.`as`(MemberReceiveArticle::dayCol.name)
)
.from(mappingWorkbookArticleT)
.where(
mappingWorkbookArticleT.WORKBOOK_ID.eq(workbookId)
)
.and(mappingWorkbookArticleT.DAY_COL.`in`(dayCols))
.and(mappingWorkbookArticleT.DELETED_AT.isNull)
.fetchInto(MemberReceiveArticle::class.java)
}.flatMap { it.stream() }.toList().let {
MemberReceiveArticles(it)
}

/** 구독자들이 받을 학습지 정보를 기준으로 학습지 관련 정보를 조회한다.*/
val articleContents = dslContext.select(
articleIfoT.ARTICLE_MST_ID.`as`(ArticleContent::id.name),
articleIfoT.CONTENT.`as`(ArticleContent::articleContent.name),
articleMstT.TITLE.`as`(ArticleContent::articleTitle.name),
articleMstT.CATEGORY_CD.`as`(ArticleContent::category.name),
DSL.jsonGetAttributeAsText(memberT.DESCRIPTION, "name").`as`(ArticleContent::writerName.name),
DSL.jsonGetAttribute(memberT.DESCRIPTION, "url").`as`(ArticleContent::writerLink.name)
)
.from(articleIfoT)
.join(articleMstT)
.on(articleIfoT.ARTICLE_MST_ID.eq(articleMstT.ID))
.join(memberT)
.on(
articleMstT.MEMBER_ID.eq(memberT.ID)
.and(memberT.TYPE_CD.eq(BatchMemberType.WRITER.code))
)
.where(articleIfoT.ARTICLE_MST_ID.`in`(memberReceiveArticles.getArticleIds()))
.and(articleIfoT.DELETED_AT.isNull)
.fetchInto(ArticleContent::class.java)

// todo fix
val memberSuccessRecords = memberIds.associateWith { true }.toMutableMap()
val failRecords = mutableMapOf<String, ArrayList<Map<Long, String>>>()
// todo check !! target is not null
val date = LocalDate.now()
val emailServiceArgs = items.map {
val toEmail = memberEmailRecords[it.memberId]!!
val memberArticle = memberReceiveArticles.getByWorkBookIdAndDayCol(it.targetWorkBookId, it.progress + 1)
val articleContent = articleContents.peek(memberArticle.articleId).let { article ->
Content(
articleLink = URL("https://www.fewletter.com/article/${memberArticle.articleId}"),
currentDate = date,
category = BatchCategoryType.convertToDisplayName(article.category.toByte()),
articleDay = memberArticle.dayCol.toInt(),
articleTitle = article.articleTitle,
writerName = article.writerName,
writerLink = article.writerLink,
articleContent = article.articleContent,
problemLink = URL("https://www.fewletter.com/problem?articleId=${memberArticle.articleId}"),
unsubscribeLink = URL("https://www.fewletter.com/unsubscribe?user=${memberEmailRecords[it.memberId]}&workbookId=${it.targetWorkBookId}&articleId=${memberArticle.articleId}")
)
}
return@map it.memberId to
SendArticleEmailArgs(
toEmail,
ARTICLE_SUBJECT_TEMPLATE.format(memberArticle.dayCol, articleContent.articleTitle),
ARTICLE_TEMPLATE,
articleContent
)
}

// todo refactoring to send email in parallel or batch
/** 이메일 전송을 위한 데이터 조회 */
val memberEmailRecords = browseMemberEmailService.execute(memberIds)
val workbooksMappedLastDayCol = browseWorkbookLastDayColService.execute(targetWorkBookIds)
val memberReceiveArticles =
browseMemberReceiveArticlesService.execute(targetWorkBookProgress)
val articleContents = browseArticleContentsService.execute(memberReceiveArticles.getArticleIds())
Comment on lines +41 to +46
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

필요한 정보를 조회하는 과정은 browseXXXService로 분리하였습니다.


/** 조회한 데이터를 이용하여 이메일 전송을 위한 인자 생성 */
val emailServiceArgs = MailServiceArgsGenerator(
LocalDate.now(),
items,
memberEmailRecords,
memberReceiveArticles,
articleContents
).generate()
Comment on lines +49 to +55
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

MailServiceArgsGenerator로 MailServiceArgs를 생성하도록 수정하였습니다.


/** 이메일 전송 */
val mailSendRecorder = MailSendRecorder(emailServiceArgs)
emailServiceArgs.forEach {
try {
sendArticleEmailService.send(it.second)
sendArticleEmailService.send(it.sendArticleEmailArgs)
} catch (e: Exception) {
memberSuccessRecords[it.first] = false
failRecords["EmailSendFail"] = failRecords.getOrDefault("EmailSendFail", arrayListOf()).apply {
val message = e.message ?: "Unknown Error"
add(mapOf(it.first to message))
}
mailSendRecorder.recordFail(
it.memberId,
it.workbookId,
e.message ?: "Unknown Error"
)
}
}

/** 워크북 마지막 학습지 DAY_COL을 조회한다 */
val lastDayCol = dslContext.select(
mappingWorkbookArticleT.WORKBOOK_ID,
DSL.max(mappingWorkbookArticleT.DAY_COL)
)
.from(mappingWorkbookArticleT)
.where(mappingWorkbookArticleT.WORKBOOK_ID.`in`(targetWorkBookIds))
.and(mappingWorkbookArticleT.DELETED_AT.isNull)
.groupBy(mappingWorkbookArticleT.WORKBOOK_ID)
.fetch()
.intoMap(mappingWorkbookArticleT.WORKBOOK_ID, DSL.max(mappingWorkbookArticleT.DAY_COL))

/** 마지막 학습지를 받은 구독자들의 ID를 필터링한다.*/
val receiveLastDayMembers = items.filter {
it.targetWorkBookId in lastDayCol.keys
}.filter {
(it.progress.toInt() + 1) == lastDayCol[it.targetWorkBookId]
}.map {
ReceiveLastDayMember(it.memberId, it.targetWorkBookId)
}
/** 이메일 전송 결과에 따라 진행률 업데이트 및 구독 해지 처리를 위한 데이터 생성 */
val receiveLastDayRecords =
ReceiveLastArticleRecordFilter(items, workbooksMappedLastDayCol).filter()
.map {
ReceiveLastArticleRecord(it.memberId, it.targetWorkBookId)
}

val receiveLastDayMemberIds = receiveLastDayMembers.map {
it.memberId
val updateTargetMemberRecords = UpdateProgressRecordFilter(
items,
mailSendRecorder.getSuccessMemberIds(),
receiveLastDayRecords.getMemberIds()
).filter().map {
UpdateProgressRecord(it.memberId, it.targetWorkBookId, it.progress)
Comment on lines +71 to +83
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

item에서 원하는 결과만 필터링하는 것ㅇ르 xxxFilter 클래스로 분리하였습니다.

}

/** 이메일 전송에 성공한 구독자들의 진행률을 업데이트한다.*/
val successMemberIds = memberSuccessRecords.filter { it.value }.keys
val updateTargetMemberRecords = items.filter { it.memberId in successMemberIds }.filterNot { it.memberId in receiveLastDayMemberIds }

/** 진행률 업데이트 */
val updateQueries = mutableListOf<UpdateConditionStep<*>>()
for (updateTargetMemberRecord in updateTargetMemberRecords) {
updateQueries.add(
dslContext.update(subscriptionT)
.set(subscriptionT.PROGRESS, updateTargetMemberRecord.progress + 1)
.where(subscriptionT.MEMBER_ID.eq(updateTargetMemberRecord.memberId))
.and(subscriptionT.TARGET_WORKBOOK_ID.eq(updateTargetMemberRecord.targetWorkBookId))
dslContext.update(Subscription.SUBSCRIPTION)
.set(Subscription.SUBSCRIPTION.PROGRESS, updateTargetMemberRecord.updatedProgress)
.where(Subscription.SUBSCRIPTION.MEMBER_ID.eq(updateTargetMemberRecord.memberId))
.and(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(updateTargetMemberRecord.targetWorkBookId))
)
}
dslContext.batch(updateQueries).execute()

/** 마지막 학습지를 받은 구독자들은 구독을 해지한다.*/
/** 학습지의 마지막 아티클을 받은 구독자 구독 해지 */
val receiveLastDayQueries = mutableListOf<UpdateConditionStep<*>>()
for (receiveLastDayMember in receiveLastDayMembers) {
for (receiveLastDayMember in receiveLastDayRecords) {
receiveLastDayQueries.add(
dslContext.update(subscriptionT)
.set(subscriptionT.DELETED_AT, LocalDateTime.now())
.set(subscriptionT.UNSUBS_OPINION, "receive.all")
.where(subscriptionT.MEMBER_ID.eq(receiveLastDayMember.memberId))
.and(subscriptionT.TARGET_WORKBOOK_ID.eq(receiveLastDayMember.targetWorkBookId))
dslContext.update(Subscription.SUBSCRIPTION)
.set(Subscription.SUBSCRIPTION.DELETED_AT, LocalDateTime.now())
.set(Subscription.SUBSCRIPTION.UNSUBS_OPINION, "receive.all")
.where(Subscription.SUBSCRIPTION.MEMBER_ID.eq(receiveLastDayMember.memberId))
.and(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(receiveLastDayMember.targetWorkBookId))
)
}
dslContext.batch(receiveLastDayQueries).execute()

return if (failRecords.isNotEmpty()) {
mapOf("records" to memberSuccessRecords, "fail" to failRecords)
} else {
mapOf("records" to memberSuccessRecords)
}
return mailSendRecorder.getExecutionResult()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.few.batch.service.article.writer.model

fun List<ReceiveLastArticleRecord>.getMemberIds(): Set<Long> {
return this.map { it.memberId }.toSet()
}

/** 학습지의 마지막 아티클을 받은 구독자 정보 */
data class ReceiveLastArticleRecord(
val memberId: Long,
val targetWorkBookId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.few.batch.service.article.writer.model

import com.few.batch.service.article.dto.WorkBookSubscriberItem

/**
* 학습지의 마지막 아티클을 받은 구독자 정보 필터
*/
class ReceiveLastArticleRecordFilter(
private val items: List<WorkBookSubscriberItem>,
private val workbooksMappedLastDayCol: Map<Long, Int>,
) {

fun filter(): List<WorkBookSubscriberItem> {
return items.filter {
it.targetWorkBookId in workbooksMappedLastDayCol.keys
}.filter {
(it.progress.toInt() + 1) == workbooksMappedLastDayCol[it.targetWorkBookId]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.few.batch.service.article.writer.model

data class UpdateProgressRecord(
val memberId: Long,
val targetWorkBookId: Long,
val progress: Long,
) {
val updatedProgress: Long = progress + 1
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

https://kapentaz.github.io/kotlin/Kotlin%EC%97%90%EC%84%9C-property%EC%99%80-function%EC%9D%98-%EC%82%AC%EC%9A%A9-%EA%B5%AC%EB%B6%84/#

해당 값의 경우 프로퍼티가 적절할 것 같아 프로퍼티로 구현하였습니다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.few.batch.service.article.writer.model

import com.few.batch.service.article.dto.WorkBookSubscriberItem

/**
* 진행률을 업데이트할 구독자 정보 필터
* - 학습지를 성공적으로 받은 구독자만 진행률을 업데이트한다.
* - 학습지의 마지막 아티클을 받은 구독자는 진행률을 업데이트하지 않고 구독을 해지한다.
*/
class UpdateProgressRecordFilter(
private val items: List<WorkBookSubscriberItem>,
private val successMemberIds: Set<Long>,
private val receiveLastDayArticleRecordMemberIds: Set<Long>,
) {
fun filter(): List<WorkBookSubscriberItem> {
return items.filter { it.memberId in successMemberIds }.filterNot { it.memberId in receiveLastDayArticleRecordMemberIds }
}
}
Loading
Loading