Skip to content

Commit

Permalink
[Refactor/#351] WorkBookSubscriberWriter 책임 분리 리펙토링 (#352)
Browse files Browse the repository at this point in the history
* feat: Record 모델및 필터 구현

* feat: Browse 서비스 구현

* feat: MailSendRecorder 구현

* feat: MailServiceArgsGenerator 구현

* refactor: WorkBookSubscriberWriter 책임 분리

* chore: 불필요 테스트 삭제

* test: 필터 클래스 테스트 추가
  • Loading branch information
belljun3395 authored Aug 19, 2024
1 parent 8a23160 commit 4b11861
Show file tree
Hide file tree
Showing 23 changed files with 454 additions and 746 deletions.
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())

/** 조회한 데이터를 이용하여 이메일 전송을 위한 인자 생성 */
val emailServiceArgs = MailServiceArgsGenerator(
LocalDate.now(),
items,
memberEmailRecords,
memberReceiveArticles,
articleContents
).generate()

/** 이메일 전송 */
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)
}

/** 이메일 전송에 성공한 구독자들의 진행률을 업데이트한다.*/
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
}
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

0 comments on commit 4b11861

Please sign in to comment.