diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt index 79686117..df988f43 100644 --- a/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt @@ -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, -) { - 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 { - 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.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): Map { - 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>>() - // 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>() 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>() - 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() } } \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecord.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecord.kt new file mode 100644 index 00000000..0a239b05 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecord.kt @@ -0,0 +1,11 @@ +package com.few.batch.service.article.writer.model + +fun List.getMemberIds(): Set { + return this.map { it.memberId }.toSet() +} + +/** 학습지의 마지막 아티클을 받은 구독자 정보 */ +data class ReceiveLastArticleRecord( + val memberId: Long, + val targetWorkBookId: Long, +) \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilter.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilter.kt new file mode 100644 index 00000000..5d55380d --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilter.kt @@ -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, + private val workbooksMappedLastDayCol: Map, +) { + + fun filter(): List { + return items.filter { + it.targetWorkBookId in workbooksMappedLastDayCol.keys + }.filter { + (it.progress.toInt() + 1) == workbooksMappedLastDayCol[it.targetWorkBookId] + } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecord.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecord.kt new file mode 100644 index 00000000..794037ee --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecord.kt @@ -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 +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilter.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilter.kt new file mode 100644 index 00000000..6e9a53bc --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilter.kt @@ -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, + private val successMemberIds: Set, + private val receiveLastDayArticleRecordMemberIds: Set, +) { + fun filter(): List { + return items.filter { it.memberId in successMemberIds }.filterNot { it.memberId in receiveLastDayArticleRecordMemberIds } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseArticleContentsService.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseArticleContentsService.kt new file mode 100644 index 00000000..85375c5d --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseArticleContentsService.kt @@ -0,0 +1,53 @@ +package com.few.batch.service.article.writer.service + +import com.few.batch.data.common.code.BatchMemberType +import jooq.jooq_dsl.tables.ArticleIfo +import jooq.jooq_dsl.tables.ArticleMst +import jooq.jooq_dsl.tables.Member +import org.jooq.DSLContext +import org.jooq.impl.DSL +import org.springframework.stereotype.Service +import java.net.URL + +data class ArticleContent( + val id: Long, + val category: String, + val articleTitle: String, + val articleContent: String, + val writerName: String, + val writerLink: URL, +) + +fun List.peek(articleId: Long): ArticleContent { + return this.find { + it.id == articleId + } ?: throw IllegalArgumentException("Cannot find article by articleId: $articleId") +} + +@Service +class BrowseArticleContentsService( + private val dslContext: DSLContext, +) { + /** 구독자들이 받을 아티클 정보를 조회한다 */ + fun execute(articleIds: List): List { + return dslContext.select( + ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.`as`(ArticleContent::id.name), + ArticleIfo.ARTICLE_IFO.CONTENT.`as`(ArticleContent::articleContent.name), + ArticleMst.ARTICLE_MST.TITLE.`as`(ArticleContent::articleTitle.name), + ArticleMst.ARTICLE_MST.CATEGORY_CD.`as`(ArticleContent::category.name), + DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(ArticleContent::writerName.name), + DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(ArticleContent::writerLink.name) + ) + .from(ArticleIfo.ARTICLE_IFO) + .join(ArticleMst.ARTICLE_MST) + .on(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.eq(ArticleMst.ARTICLE_MST.ID)) + .join(Member.MEMBER) + .on( + ArticleMst.ARTICLE_MST.MEMBER_ID.eq(Member.MEMBER.ID) + .and(Member.MEMBER.TYPE_CD.eq(BatchMemberType.WRITER.code)) + ) + .where(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.`in`(articleIds)) + .and(ArticleIfo.ARTICLE_IFO.DELETED_AT.isNull) + .fetchInto(ArticleContent::class.java) + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt new file mode 100644 index 00000000..3b3f2ce6 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt @@ -0,0 +1,22 @@ +package com.few.batch.service.article.writer.service + +import jooq.jooq_dsl.tables.Member +import org.jooq.DSLContext +import org.springframework.stereotype.Service + +@Service +class BrowseMemberEmailService( + private val dslContext: DSLContext, +) { + /** 회원 ID를 기준으로 이메일을 조회한다.*/ + fun execute(memberIds: Set): Map { + return dslContext.select( + Member.MEMBER.ID, + Member.MEMBER.EMAIL + ) + .from(Member.MEMBER) + .where(Member.MEMBER.ID.`in`(memberIds)) + .fetch() + .intoMap(Member.MEMBER.ID, Member.MEMBER.EMAIL) + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberReceiveArticlesService.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberReceiveArticlesService.kt new file mode 100644 index 00000000..079b1c63 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberReceiveArticlesService.kt @@ -0,0 +1,55 @@ +package com.few.batch.service.article.writer.service + +import jooq.jooq_dsl.tables.MappingWorkbookArticle +import org.jooq.DSLContext +import org.springframework.stereotype.Service + +data class MemberReceiveArticle( + val workbookId: Long, + val articleId: Long, + val dayCol: Long, +) + +data class MemberReceiveArticles( + val articles: List, +) { + 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 { + return articles.map { + it.articleId + } + } +} + +@Service +class BrowseMemberReceiveArticlesService( + private val dslContext: DSLContext, +) { + /** 회원들이 구독한 학습지와 학습 진행 상태를 기준으로 회원들이 받아야 하는 아티클 정보를 조회한다 */ + fun execute(workbooks: Map>): MemberReceiveArticles { + return workbooks.entries.stream().map { (workbookId, progress) -> + val dayCols = progress.stream().map { it + 1L }.toList() + dslContext.select( + MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.`as`( + MemberReceiveArticle::workbookId.name + ), + MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.`as`(MemberReceiveArticle::articleId.name), + MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.`as`(MemberReceiveArticle::dayCol.name) + ) + .from(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) + .where( + MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(workbookId) + ) + .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.`in`(dayCols)) + .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull) + .fetchInto(MemberReceiveArticle::class.java) + }.flatMap { it.stream() }.toList().let { + MemberReceiveArticles(it) + } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseWorkbookLastDayColService.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseWorkbookLastDayColService.kt new file mode 100644 index 00000000..1d368f59 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseWorkbookLastDayColService.kt @@ -0,0 +1,28 @@ +package com.few.batch.service.article.writer.service + +import jooq.jooq_dsl.tables.MappingWorkbookArticle +import org.jooq.DSLContext +import org.jooq.impl.DSL +import org.springframework.stereotype.Service + +@Service +class BrowseWorkbookLastDayColService( + private val dslContext: DSLContext, +) { + /** 워크북의 마지막 아티클의 day_col을 조회한다 */ + fun execute(workbookIds: Set): Map { + return dslContext.select( + MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID, + DSL.max(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL) + ) + .from(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) + .where(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.`in`(workbookIds)) + .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull) + .groupBy(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID) + .fetch() + .intoMap( + MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID, + DSL.max(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL) + ) + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/support/MailSendRecorder.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/support/MailSendRecorder.kt new file mode 100644 index 00000000..99ab57c7 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/support/MailSendRecorder.kt @@ -0,0 +1,40 @@ +package com.few.batch.service.article.writer.support + +private data class MemberMappedWorkBook( + val memberId: Long, + val workbookId: Long, +) + +/** 이메일 전송 성공/실패 기록을 위한 정보 기록 클래스 */ +class MailSendRecorder(mailServiceArgs: List) { + + private var records: MutableMap + private var failRecords: MutableMap> + + init { + records = + mailServiceArgs.associate { MemberMappedWorkBook(it.memberId, it.workbookId) to true }.toMutableMap() + failRecords = mutableMapOf() + } + + fun recordFail(memberId: Long, workbookId: Long, reason: String) { + records[MemberMappedWorkBook(memberId, workbookId)] = false + failRecords[reason] = failRecords.getOrDefault(reason, arrayListOf()).apply { + add(MemberMappedWorkBook(memberId, workbookId)) + } + } + + fun getSuccessMemberIds(): Set { + return records.filter { it.value }.keys.map { it.memberId }.toSet() + } + + fun getExecutionResult(): Map { + val executionRecords = records.keys.groupBy { it.memberId } + .mapValues { it -> it.value.associate { it.workbookId to records[it] } } + return if (failRecords.isNotEmpty()) { + mapOf("records" to executionRecords, "fail" to failRecords) + } else { + mapOf("records" to executionRecords) + } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/support/MailServiceArgsGenerator.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/support/MailServiceArgsGenerator.kt new file mode 100644 index 00000000..4ca59c10 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/support/MailServiceArgsGenerator.kt @@ -0,0 +1,69 @@ +package com.few.batch.service.article.writer.support + +import com.few.batch.data.common.code.BatchCategoryType +import com.few.batch.service.article.dto.WorkBookSubscriberItem +import com.few.batch.service.article.writer.service.ArticleContent +import com.few.batch.service.article.writer.service.MemberReceiveArticles +import com.few.batch.service.article.writer.service.peek +import com.few.email.service.article.dto.Content +import com.few.email.service.article.dto.SendArticleEmailArgs +import java.net.URL +import java.time.LocalDate + +data class MailServiceArg( + val memberId: Long, + val workbookId: Long, + val sendArticleEmailArgs: SendArticleEmailArgs, +) + +class MailServiceArgsGenerator( + private val date: LocalDate, + private val items: List, + private val memberEmailRecords: Map, + private val memberReceiveArticles: MemberReceiveArticles, + private val articleContents: List, +) { + companion object { + private const val ARTICLE_SUBJECT_TEMPLATE = "Day%d %s" + private const val ARTICLE_TEMPLATE = "article" + } + + /** + * 이메일 전송을 위한 인자 생성 + */ + fun generate(): List { + return 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}") + ) + } + + MailServiceArg( + it.memberId, + it.targetWorkBookId, + SendArticleEmailArgs( + toEmail, + ARTICLE_SUBJECT_TEMPLATE.format( + memberArticle.dayCol, + articleContent.articleTitle + ), + ARTICLE_TEMPLATE, + articleContent + ) + ) + } + } +} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/BatchTestContainerInitializer.kt b/batch/src/test/kotlin/com/few/batch/BatchTestContainerInitializer.kt deleted file mode 100644 index 735951a6..00000000 --- a/batch/src/test/kotlin/com/few/batch/BatchTestContainerInitializer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.few.batch - -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.context.ApplicationContextInitializer -import org.springframework.context.ConfigurableApplicationContext -import org.testcontainers.containers.DockerComposeContainer -import java.io.File - -class BatchTestContainerInitializer : ApplicationContextInitializer { - private val log = KotlinLogging.logger {} - - companion object { - private const val MYSQL = "mysql" - private const val MYSQL_PORT = 3306 - - private val dockerCompose = - DockerComposeContainer(File("src/test/resources/docker-compose.yml")) - .withExposedService(MYSQL, MYSQL_PORT) - } - - override fun initialize(applicationContext: ConfigurableApplicationContext) { - log.debug { "===== set up test containers =====" } - - dockerCompose.start() - - log.debug { "===== success set up test containers =====" } - } -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/BatchTestDataSourceConfig.kt b/batch/src/test/kotlin/com/few/batch/BatchTestDataSourceConfig.kt deleted file mode 100644 index 869767d8..00000000 --- a/batch/src/test/kotlin/com/few/batch/BatchTestDataSourceConfig.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.few.batch - -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.boot.jdbc.DataSourceBuilder -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.jdbc.datasource.DataSourceTransactionManager -import org.springframework.transaction.PlatformTransactionManager -import javax.sql.DataSource - -@TestConfiguration -class BatchTestDataSourceConfig { - - @Bean - @ConfigurationProperties(prefix = "spring.datasource") - fun batchTestDataSource(): DataSource { - return DataSourceBuilder.create().build() - } - - @Bean - fun batchTestTransactionManager(): PlatformTransactionManager { - return DataSourceTransactionManager(batchTestDataSource()) - } -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/BatchTestFlywayConfig.kt b/batch/src/test/kotlin/com/few/batch/BatchTestFlywayConfig.kt deleted file mode 100644 index 63462c68..00000000 --- a/batch/src/test/kotlin/com/few/batch/BatchTestFlywayConfig.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.few.batch - -import org.flywaydb.core.Flyway -import org.flywaydb.core.api.configuration.FluentConfiguration -import org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializer -import org.springframework.boot.autoconfigure.flyway.FlywayProperties -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Import -import java.util.function.Consumer -import javax.sql.DataSource - -@Configuration -@Import(BatchTestDataSourceConfig::class) -class BatchTestFlywayConfig { - @Bean - fun flyway( - configuration: org.flywaydb.core.api.configuration.Configuration?, - ): Flyway { - return Flyway(configuration) - } - - @Bean - fun flywayMigrationInitializer( - flyway: Flyway?, - ): FlywayMigrationInitializer { - return FlywayMigrationInitializer(flyway) { obj: Flyway -> obj.migrate() } - } - - @Bean - @ConfigurationProperties(prefix = "spring.flyway") - fun flywayProperties(): FlywayProperties { - return FlywayProperties() - } - - @Bean - fun configuration( - dataSource: DataSource?, - ): org.flywaydb.core.api.configuration.Configuration { - val configuration = FluentConfiguration() - configuration.dataSource(dataSource) - flywayProperties().locations.forEach( - Consumer { locations: String? -> - configuration.locations( - locations - ) - } - ) - return configuration - } -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/BatchTestSpec.kt b/batch/src/test/kotlin/com/few/batch/BatchTestSpec.kt deleted file mode 100644 index c4c07dee..00000000 --- a/batch/src/test/kotlin/com/few/batch/BatchTestSpec.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.few.batch - -import com.fasterxml.jackson.databind.ObjectMapper -import com.few.batch.config.BatchConfig -import com.few.email.service.article.SendArticleEmailService -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.mock.mockito.MockBean -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration - -@ActiveProfiles("new", "test") -@SpringBootTest(classes = [BatchConfig::class, ObjectMapper::class]) -@ContextConfiguration(initializers = [BatchTestContainerInitializer::class]) -abstract class BatchTestSpec { - - @MockBean - lateinit var sendArticleEmailService: SendArticleEmailService -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/log/BatchCallExecutionServiceTest.kt b/batch/src/test/kotlin/com/few/batch/log/BatchCallExecutionServiceTest.kt deleted file mode 100644 index 503516b7..00000000 --- a/batch/src/test/kotlin/com/few/batch/log/BatchCallExecutionServiceTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.few.batch.log - -import com.fasterxml.jackson.databind.ObjectMapper -import com.few.batch.BatchTestSpec -import jooq.jooq_dsl.Tables.BATCH_CALL_EXECUTION -import org.jooq.DSLContext -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired - -class BatchCallExecutionServiceTest : BatchTestSpec() { - - @Autowired - private lateinit var dslContext: DSLContext - - @Autowired - private lateinit var objectMapper: ObjectMapper - - @Autowired - private lateinit var batchCallExecutionService: BatchCallExecutionService - - @BeforeEach - fun setUp() { - dslContext.deleteFrom(BATCH_CALL_EXECUTION).execute() - } - - @Test - fun `배치 결과가 성공만 있는 경우`() { - // given - val status = true - val descriptionSource = mapOf("success" to listOf(1, 2, 3)) - val description = objectMapper.writeValueAsString(descriptionSource) - - // when - batchCallExecutionService.execute(status, description) - - // then - val result = dslContext.select(BATCH_CALL_EXECUTION.STATUS, BATCH_CALL_EXECUTION.DESCRIPTION) - .from(BATCH_CALL_EXECUTION) - .fetch() - - assertEquals(1, result.size) - assertEquals(status, result[0].value1()) - objectMapper.readTree(description).let { expected -> - objectMapper.readTree(result[0].value2().data()).let { actual -> - assertEquals(expected, actual) - } - } - } - - @Test - fun `배치 결과가 실패만 있는 경우`() { - // given - val status = false - val descriptionSource = mapOf("fail" to mapOf("EmailSendFail" to listOf(1, 2, 3))) - val description = objectMapper.writeValueAsString(descriptionSource) - - // when - batchCallExecutionService.execute(status, description) - - // then - val result = dslContext.select(BATCH_CALL_EXECUTION.STATUS, BATCH_CALL_EXECUTION.DESCRIPTION) - .from(BATCH_CALL_EXECUTION) - .fetch() - - assertEquals(1, result.size) - assertEquals(status, result[0].value1()) - objectMapper.readTree(description).let { expected -> - objectMapper.readTree(result[0].value2().data()).let { actual -> - assertEquals(expected, actual) - } - } - } - - @Test - fun `배치 결과가 성공과 실패가 섞여 있는 경우`() { - // given - val status = false - val descriptionSource = mapOf("success" to listOf(1, 2, 3), "fail" to mapOf("EmailSendFail" to listOf(4, 5, 6))) - val description = objectMapper.writeValueAsString(descriptionSource) - - // when - batchCallExecutionService.execute(status, description) - - // then - val result = dslContext.select(BATCH_CALL_EXECUTION.STATUS, BATCH_CALL_EXECUTION.DESCRIPTION) - .from(BATCH_CALL_EXECUTION) - .fetch() - - assertEquals(1, result.size) - assertEquals(status, result[0].value1()) - objectMapper.readTree(description).let { expected -> - objectMapper.readTree(result[0].value2().data()).let { actual -> - assertEquals(expected, actual) - } - } - } -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTest.kt b/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTest.kt deleted file mode 100644 index 49512008..00000000 --- a/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.few.batch.service.article.writer - -import com.few.batch.BatchTestSpec -import com.few.email.service.article.dto.SendArticleEmailArgs -import jooq.jooq_dsl.tables.ArticleIfo -import jooq.jooq_dsl.tables.ArticleMst -import jooq.jooq_dsl.tables.MappingWorkbookArticle -import jooq.jooq_dsl.tables.Member -import jooq.jooq_dsl.tables.Subscription -import org.jooq.DSLContext -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.mockito.Mockito.* -import org.springframework.beans.factory.annotation.Autowired -import kotlin.random.Random - -@Disabled -class WorkBookSubscriberWriterTest : BatchTestSpec() { - - @Autowired - private lateinit var helper: WorkBookSubscriberWriterTestSetHelper - - @Autowired - private lateinit var dslContext: DSLContext - - @Autowired - private lateinit var workBookSubscriberWriter: WorkBookSubscriberWriter - - @BeforeEach - fun setUp() { - dslContext.deleteFrom(Member.MEMBER).execute() - dslContext.deleteFrom(Subscription.SUBSCRIPTION).execute() - dslContext.deleteFrom(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE).execute() - dslContext.deleteFrom(ArticleIfo.ARTICLE_IFO).execute() - dslContext.deleteFrom(ArticleMst.ARTICLE_MST).execute() - - // setup member - helper.setUpMembers(10) - helper.setUpWriter(11) - - // setup subscription - helper.setUpSubscriptions(start = 1, count = 5, workbookId = 1) - helper.setUpSubscriptions(start = 6, count = 10, workbookId = 2) - - // setup article mst - helper.setUpArticleMst(start = 1, count = 10, writerId = 11) - - // setup mapping workbook article - helper.setUpMappingWorkbookArticle(start = 1, count = 5, workbookId = 1) - helper.setUpMappingWorkbookArticle(start = 6, count = 10, workbookId = 2, dayCorrection = 5) - - // setup article ifo - helper.setUpArticleIfo(start = 1, count = 10) - } - - @Test - fun `WorkBookSubscriberWriter가 모두 정상적으로 동작하는 경우`() { - // given - val items = helper.browseItems(listOf(1, 2)) - - // when - workBookSubscriberWriter.execute(items.toServiceDto()) - - // then - val results = dslContext.select(Subscription.SUBSCRIPTION.MEMBER_ID, Subscription.SUBSCRIPTION.PROGRESS) - .from(Subscription.SUBSCRIPTION) - .where(Subscription.SUBSCRIPTION.MEMBER_ID.`in`(items.toMemberIds())) - .fetch() - .intoMap(Subscription.SUBSCRIPTION.MEMBER_ID, Subscription.SUBSCRIPTION.PROGRESS) - - assertEquals(10, results.size) - items.forEach { - assertEquals(it.progress + 1, results[it.memberId]) - } - } - - @Test - fun `WorkBookSubscriberWriter가 일부 이메일 전송 과정에서 비정상적으로 동작하는 경우`() { - // given - val items = helper.browseItems(listOf(1, 2)) - val failItem = items[Random.nextInt(6, items.size - 1)] - val failArgs = SendArticleEmailArgs( - "member${failItem.memberId}@gmail.com", - "Day${failItem.content.articleDay} ${failItem.content.articleTitle}", - "article", - failItem.content, - "" - ) - `when`( - sendArticleEmailService.send(failArgs) - ).thenThrow(RuntimeException("send email error")) - - // when - workBookSubscriberWriter.execute(items.toServiceDto()) - - // then - val results = dslContext.select(Subscription.SUBSCRIPTION.MEMBER_ID, Subscription.SUBSCRIPTION.PROGRESS) - .from(Subscription.SUBSCRIPTION) - .where(Subscription.SUBSCRIPTION.MEMBER_ID.`in`(items.toMemberIds())) - .fetch() - .intoMap(Subscription.SUBSCRIPTION.MEMBER_ID, Subscription.SUBSCRIPTION.PROGRESS) - - assertEquals(10, results.size) - items.forEach { - if (failItem.memberId == it.memberId) { - assertEquals(it.progress, results[it.memberId]) - } else { - assertEquals(it.progress + 1, results[it.memberId]) - } - } - } -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt b/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt deleted file mode 100644 index e3e96e8d..00000000 --- a/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt +++ /dev/null @@ -1,162 +0,0 @@ -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.email.service.article.dto.Content -import jooq.jooq_dsl.tables.* -import org.jooq.DSLContext -import org.jooq.JSON -import org.springframework.boot.test.context.TestComponent -import java.net.URL -import java.time.LocalDate -import kotlin.random.Random - -fun List.toServiceDto(): List { - return this.map { - WorkBookSubscriberItem(it.memberId, it.targetWorkBookId, it.progress) - } -} - -fun List.toMemberIds(): List { - return this.map { it.memberId } -} - -data class TestWorkBookSubscriberDto( - val memberId: Long, - val targetWorkBookId: Long, - val progress: Long, - val content: Content, -) - -data class ArticleDto( - val articleId: Long, - val dayCol: Int, - val content: String, - val title: String, -) - -@TestComponent -class WorkBookSubscriberWriterTestSetHelper( - private val dslContext: DSLContext, -) { - - fun setUpMembers(count: Int) { - for (i in 1..count) { - dslContext.insertInto(Member.MEMBER) - .set(Member.MEMBER.ID, i.toLong()) - .set(Member.MEMBER.EMAIL, "member$i@gmail.com") - .set(Member.MEMBER.TYPE_CD, BatchMemberType.NORMAL.code) - .execute() - } - } - - fun setUpWriter(id: Long) { - dslContext.insertInto(Member.MEMBER) - .set(Member.MEMBER.ID, id) - .set(Member.MEMBER.EMAIL, "writer@gmail.com") - .set(Member.MEMBER.TYPE_CD, BatchMemberType.WRITER.code) - .set(Member.MEMBER.DESCRIPTION, JSON.valueOf("{\"url\": \"http://localhost:8080\", \"name\": \"writer\"}")) - .execute() - } - - fun setUpSubscriptions(start: Int = 1, count: Int, workbookId: Long = 1) { - for (i in start..count) { - dslContext.insertInto(Subscription.SUBSCRIPTION) - .set(Subscription.SUBSCRIPTION.MEMBER_ID, i.toLong()) - .set(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID, workbookId) - .set(Subscription.SUBSCRIPTION.PROGRESS, Random.nextLong(1, count.toLong() - start.toLong())) - .execute() - } - } - - fun setUpMappingWorkbookArticle(start: Int = 1, count: Int, workbookId: Long = 1, dayCorrection: Int = 1) { - for (i in start..count) { - dslContext.insertInto(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) - .set(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID, workbookId) - .set(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID, i.toLong()) - .set(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL, i - dayCorrection) - .execute() - } - } - - fun setUpArticleMst(start: Int = 1, count: Int, writerId: Long) { - for (i in start..count) { - dslContext.insertInto(ArticleMst.ARTICLE_MST) - .set(ArticleMst.ARTICLE_MST.ID, i.toLong()) - .set(ArticleMst.ARTICLE_MST.TITLE, "title$i") - .set(ArticleMst.ARTICLE_MST.MEMBER_ID, writerId) - .set(ArticleMst.ARTICLE_MST.CATEGORY_CD, BatchCategoryType.fromCode(0)!!.code) - .set(ArticleMst.ARTICLE_MST.MAIN_IMAGE_URL, "http://localhost:8080") - .execute() - } - } - - fun setUpArticleIfo(start: Int = 1, count: Int) { - for (i in start..count) { - dslContext.insertInto(ArticleIfo.ARTICLE_IFO) - .set(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID, i.toLong()) - .set(ArticleIfo.ARTICLE_IFO.CONTENT, "content$i") - .execute() - } - } - - fun browseItems(targetWorkBookIds: List): List { - val items = mutableListOf() - targetWorkBookIds.forEach { - workbookId -> - dslContext.select(Subscription.SUBSCRIPTION.MEMBER_ID, Subscription.SUBSCRIPTION.PROGRESS) - .from(Subscription.SUBSCRIPTION) - .where(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(workbookId)) - .fetch() - .forEach { - val content = dslContext.select( - ArticleIfo.ARTICLE_IFO.CONTENT - ) - .from(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) - .join(ArticleIfo.ARTICLE_IFO) - .on(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.eq(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID)) - .where( - MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(workbookId) - ) - .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.eq(it[Subscription.SUBSCRIPTION.PROGRESS].toInt() + 1)) - .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull) - .fetchOneInto(String::class.java) - - val articleDto = dslContext.select( - MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID, - MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL, - ArticleIfo.ARTICLE_IFO.CONTENT, - ArticleMst.ARTICLE_MST.TITLE - ).from(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) - .join(ArticleIfo.ARTICLE_IFO) - .on(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.eq(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID)) - .join(ArticleMst.ARTICLE_MST) - .on(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.eq(ArticleMst.ARTICLE_MST.ID)) - .where(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(workbookId)) - .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.eq(it[Subscription.SUBSCRIPTION.PROGRESS].toInt() + 1)) - .fetchOneInto(ArticleDto::class.java) - - val dto = TestWorkBookSubscriberDto( - memberId = it[Subscription.SUBSCRIPTION.MEMBER_ID], - targetWorkBookId = workbookId, - progress = it[Subscription.SUBSCRIPTION.PROGRESS], - content = Content( - articleLink = URL("https://www.fewletter.com/article/${articleDto!!.articleId}"), - currentDate = LocalDate.now(), - category = BatchCategoryType.fromCode(0)!!.displayName, - articleDay = articleDto.dayCol, - articleTitle = articleDto.title, - writerName = "writer", - writerLink = URL("http://localhost:8080"), - articleContent = articleDto.content, - problemLink = URL("https://www.fewletter.com/problem?articleId=${articleDto.articleId}"), - unsubscribeLink = URL("https://www.fewletter.com/unsubscribe?user=member${it[Subscription.SUBSCRIPTION.MEMBER_ID]}@gmail.com&workbookId=$workbookId&articleId=${articleDto.articleId}") - ) - ) - items.add(dto) - } - } - return items - } -} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilterTest.kt b/batch/src/test/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilterTest.kt new file mode 100644 index 00000000..12b1e928 --- /dev/null +++ b/batch/src/test/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilterTest.kt @@ -0,0 +1,31 @@ +package com.few.batch.service.article.writer.model + +import com.few.batch.service.article.dto.WorkBookSubscriberItem +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.stream.IntStream + +class ReceiveLastArticleRecordFilterTest { + + @Test + fun `학습지의 마지막 아티클을 받은 구독자 정보를 필터링 한다`() { + // given + val targetWorkbookId = 1L + val subscriberItemCount = 5 + // 가장 큰 아이디를 가진 구독자는 마지막 아티클을 받은 상태 + val items = IntStream.range(1, 1 + subscriberItemCount).mapToObj { + WorkBookSubscriberItem(it.toLong(), targetWorkbookId, it.toLong() - 1) + }.toList() + val workbooksMappedLastDayCol = mapOf(1L to subscriberItemCount) + + val filter = ReceiveLastArticleRecordFilter(items, workbooksMappedLastDayCol) + + // when + val result = filter.filter() + + // then + assertEquals(1, result.size) + assertEquals(subscriberItemCount.toLong(), result[0].memberId) + assertEquals(targetWorkbookId, result[0].targetWorkBookId) + } +} \ No newline at end of file diff --git a/batch/src/test/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilterTest.kt b/batch/src/test/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilterTest.kt new file mode 100644 index 00000000..f8c3f100 --- /dev/null +++ b/batch/src/test/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilterTest.kt @@ -0,0 +1,36 @@ +package com.few.batch.service.article.writer.model + +import com.few.batch.service.article.dto.WorkBookSubscriberItem +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.stream.Collectors.toSet +import java.util.stream.IntStream + +class UpdateProgressRecordFilterTest { + + @Test + fun `진행률을 업데이트할 구독자 정보를 필터링한다`() { + // given + val targetWorkbookId = 1L + val subscriberItemCount = 5 + // 가장 큰 아이디를 가진 구독자는 마지막 아티클을 받은 상태 + val successMemberIds = + IntStream.range(1, 1 + subscriberItemCount).mapToObj { it.toLong() }.collect(toSet()) + val receiveLastDayArticleRecordMemberIds = setOf(subscriberItemCount.toLong()) + val items = (1..subscriberItemCount).map { + WorkBookSubscriberItem(it.toLong(), targetWorkbookId, it.toLong() - 1) + } + + val filter = UpdateProgressRecordFilter(items, successMemberIds, receiveLastDayArticleRecordMemberIds) + + // when + val result = filter.filter() + + // then + assertEquals(4, result.size) + result.forEach { + assertTrue(it.memberId in successMemberIds) + assertFalse(it.memberId in receiveLastDayArticleRecordMemberIds) + } + } +} \ No newline at end of file diff --git a/batch/src/test/resources/application-test.yml b/batch/src/test/resources/application-test.yml deleted file mode 100644 index ef0e104e..00000000 --- a/batch/src/test/resources/application-test.yml +++ /dev/null @@ -1,33 +0,0 @@ -logging: - level: - org.jooq: DEBUG - org.springframework.jdbc: DEBUG - com.few.batch: DEBUG - org.testcontainers: INFO - -spring: - datasource: - jdbcUrl: jdbc:mysql://localhost:43306/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&allowMultiQueries=true - username: root - password: root - driver-class-name: com.mysql.cj.jdbc.Driver - flyway: - locations: classpath:db/migration/entity - sql-migration-suffixes: sql - baseline-on-migrate: true - baseline-version: 0 - mail: - protocol: smtp - host: smtp.gmail.com - port: 587 - username: DevFewFew@gmail.com - password: batchtest - properties: - mail: - smtp: - auth: true - debug: true - starttls: - enable: true - EnableSSL: - enable: true diff --git a/batch/src/test/resources/docker-compose.yml b/batch/src/test/resources/docker-compose.yml deleted file mode 100644 index 276d7168..00000000 --- a/batch/src/test/resources/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: '3.7' - -services: - mysql: - image: mysql/mysql-server:8.0.27 - environment: - - MYSQL_ROOT_PASSWORD=root - - MYSQL_ROOT_HOST=% - - TZ=Asia/Seoul - command: [ "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--lower_case_table_names=1", "--max_connections=2048", "--wait_timeout=3600" ] - ports: - - "43306:3306" - volumes: - - ./mysql-init.d:/docker-entrypoint-initdb.d diff --git a/batch/src/test/resources/mysql-init.d/00_init.sql b/batch/src/test/resources/mysql-init.d/00_init.sql deleted file mode 100644 index 103b9b8b..00000000 --- a/batch/src/test/resources/mysql-init.d/00_init.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE - USER 'few-test-local'@'localhost' IDENTIFIED BY 'few-test-local'; -CREATE - USER 'few-test-local'@'%' IDENTIFIED BY 'few-test-local'; - -GRANT ALL PRIVILEGES ON *.* TO - 'few-test-local'@'localhost'; -GRANT ALL PRIVILEGES ON *.* TO - 'few-test-local'@'%'; - -CREATE - DATABASE api DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;