-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from all commits
e4b460e
7132757
bcb7127
fd2a617
7314c87
ebad754
ce11fb2
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 |
---|---|---|
@@ -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() | ||
Comment on lines
+49
to
+55
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. 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
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. 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 | ||
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. 해당 값의 경우 프로퍼티가 적절할 것 같아 프로퍼티로 구현하였습니다. |
||
} |
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 } | ||
} | ||
} |
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.
필요한 정보를 조회하는 과정은 browseXXXService로 분리하였습니다.