From ac38810ea5df22fccb32dc5773011168c2691eb4 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Fri, 27 Sep 2024 23:05:54 +0900 Subject: [PATCH] =?UTF-8?q?[Feat/#412]=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20OPE?= =?UTF-8?q?N,=20DELIVERY=20DELAY=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20(#419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 전송 아티클 이벤트 테이블 추가 및 필요 enum 추가 * refactor: 배치 전송후 전송 기록 추가 * feat: 전송 아티클 이벤트 추가 및 조회 쿼리 추가 * feat: AddEmailLogUseCase 구현 * feat: ArticleLogService 구현 * feat: ArticleMemberService 구현 * feat: ReadArticleByEmailUseCase 구현 * feat: addEmailLog 구현 * feat: readArticleByEmail 구현 * fix: 요청으로 Byte를 받지 못하는 문제 해결 * feat: 아티클 로그 요청 시큐리티 해제 * fix: execute가 없어서 쿼리가 동작하지 않는 문제 해결 * fix: EmailLogEventType 변환할 때 type의 대소문자 구분하지 않고 변한할 수 있도록 수정 * feat: 로그 기록을 위한 인증 예외 주소 추가 * refactor: 아티클 이메일 전송시 전송 이벤트 기록하도록 수정 * fix: 요청을 처리할 수 있도록 수정 * fix: todo로 예외 발생하는 문제 해결 * fix: eventType이 재대로 저장되지 않는 문제 해결 --- .../dao/log/SendArticleEventHistoryDao.kt | 48 +++++++++++++++++++ .../dao/log/command/InsertEventCommand.kt | 9 ++++ ...SelectEventByMessageIdAndEventTypeQuery.kt | 6 +++ .../record/SendArticleEventHistoryRecord.kt | 9 ++++ .../article/service/ArticleLogService.kt | 36 ++++++++++++++ .../article/service/ArticleMemberService.kt | 18 +++++++ .../article/service/dto/InsertOpenEventDto.kt | 9 ++++ .../service/dto/ReadMemberByEmailDto.kt | 5 ++ .../dto/SelectDeliveryEventByMessageIdDto.kt | 6 +++ .../usecase/ReadArticleByEmailUseCase.kt | 45 +++++++++++++++++ .../dto/ReadArticleByEmailUseCaseIn.kt | 11 +++++ .../few/api/domain/log/AddEmailLogUseCase.kt | 46 ++++++++++++++++++ .../domain/log/dto/AddEmailLogUseCaseIn.kt | 11 +++++ .../SendWorkbookArticleAsyncHandler.kt | 20 ++++++-- .../service/SubscriptionEmailService.kt | 4 +- .../service/SubscriptionLogService.kt | 25 ++++++++++ .../service/dto/InsertSendEventDto.kt | 8 ++++ .../api/security/config/WebSecurityConfig.kt | 4 ++ .../com/few/api/web/config/WebConfig.kt | 6 +-- .../converter/EmailLogEventTypeConverter.kt | 11 +++++ .../web/config/converter/SendTypeConverter.kt | 10 ++++ .../web/controller/admin/ApiLogController.kt | 18 +++++++ .../admin/request/EmailLogRequest.kt | 12 +++++ .../controller/article/ArticleController.kt | 25 +++++++++- .../request/ReadArticleByEmailRequest.kt | 6 +++ .../few/api/web/support/EmailLogEventType.kt | 23 +++++++++ .../com/few/api/web/support/SendType.kt | 16 +++++++ .../data/common/code/BatchSendEventType.kt | 24 ++++++++++ .../batch/data/common/code/BatchSendType.kt | 9 ++++ .../writer/WorkBookSubscriberWriter.kt | 15 +++++- .../support/MailServiceArgsGenerator.kt | 4 +- ...__add_send_article_event_history_table.sql | 12 +++++ .../com/few/data/common/code/SendEventType.kt | 24 ++++++++++ .../com/few/data/common/code/SendType.kt | 10 ++++ 34 files changed, 532 insertions(+), 13 deletions(-) create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/log/SendArticleEventHistoryDao.kt create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/log/command/InsertEventCommand.kt create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/log/query/SelectEventByMessageIdAndEventTypeQuery.kt create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/log/record/SendArticleEventHistoryRecord.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/service/ArticleLogService.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/service/ArticleMemberService.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/service/dto/InsertOpenEventDto.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadMemberByEmailDto.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/service/dto/SelectDeliveryEventByMessageIdDto.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleByEmailUseCaseIn.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/log/AddEmailLogUseCase.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/log/dto/AddEmailLogUseCaseIn.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionLogService.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/subscription/service/dto/InsertSendEventDto.kt create mode 100644 api/src/main/kotlin/com/few/api/web/config/converter/EmailLogEventTypeConverter.kt create mode 100644 api/src/main/kotlin/com/few/api/web/config/converter/SendTypeConverter.kt create mode 100644 api/src/main/kotlin/com/few/api/web/controller/admin/request/EmailLogRequest.kt create mode 100644 api/src/main/kotlin/com/few/api/web/controller/article/request/ReadArticleByEmailRequest.kt create mode 100644 api/src/main/kotlin/com/few/api/web/support/EmailLogEventType.kt create mode 100644 api/src/main/kotlin/com/few/api/web/support/SendType.kt create mode 100644 batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendEventType.kt create mode 100644 batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendType.kt create mode 100644 data/db/migration/entity/V1.00.0.25__add_send_article_event_history_table.sql create mode 100644 data/src/main/kotlin/com/few/data/common/code/SendEventType.kt create mode 100644 data/src/main/kotlin/com/few/data/common/code/SendType.kt diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/log/SendArticleEventHistoryDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/SendArticleEventHistoryDao.kt new file mode 100644 index 00000000..b087fa9f --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/SendArticleEventHistoryDao.kt @@ -0,0 +1,48 @@ +package com.few.api.repo.dao.log + +import com.few.api.repo.dao.log.command.InsertEventCommand +import com.few.api.repo.dao.log.query.SelectEventByMessageIdAndEventTypeQuery +import com.few.api.repo.dao.log.record.SendArticleEventHistoryRecord +import jooq.jooq_dsl.tables.SendArticleEventHistory +import org.jooq.DSLContext +import org.springframework.stereotype.Repository + +@Repository +class SendArticleEventHistoryDao( + private val dslContext: DSLContext, +) { + + fun insertEvent(command: InsertEventCommand) { + dslContext.insertInto(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MEMBER_ID, command.memberId) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.ARTICLE_ID, command.articleId) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MESSAGE_ID, command.messageId) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.EVENT_TYPE_CD, command.eventType) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.SEND_TYPE_CD, command.sendType) + .execute() + } + + fun selectEventByMessageId(query: SelectEventByMessageIdAndEventTypeQuery): SendArticleEventHistoryRecord? { + return dslContext.select( + SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MEMBER_ID.`as`( + SendArticleEventHistoryRecord::memberId.name + ), + SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.ARTICLE_ID.`as`( + SendArticleEventHistoryRecord::articleId.name + ), + SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MESSAGE_ID.`as`( + SendArticleEventHistoryRecord::messageId.name + ), + SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.EVENT_TYPE_CD.`as`( + SendArticleEventHistoryRecord::eventType.name + ), + SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.SEND_TYPE_CD.`as`( + SendArticleEventHistoryRecord::sendType.name + ) + ) + .from(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY) + .where(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MESSAGE_ID.eq(query.messageId)) + .and(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.EVENT_TYPE_CD.eq(query.eventType)) + .fetchOne()?.into(SendArticleEventHistoryRecord::class.java) + } +} \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/log/command/InsertEventCommand.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/command/InsertEventCommand.kt new file mode 100644 index 00000000..63582073 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/command/InsertEventCommand.kt @@ -0,0 +1,9 @@ +package com.few.api.repo.dao.log.command + +data class InsertEventCommand( + val memberId: Long, + val articleId: Long, + val messageId: String, + val eventType: Byte, + val sendType: Byte, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/log/query/SelectEventByMessageIdAndEventTypeQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/query/SelectEventByMessageIdAndEventTypeQuery.kt new file mode 100644 index 00000000..05d96a8b --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/query/SelectEventByMessageIdAndEventTypeQuery.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.log.query + +data class SelectEventByMessageIdAndEventTypeQuery( + val messageId: String, + val eventType: Byte, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/log/record/SendArticleEventHistoryRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/record/SendArticleEventHistoryRecord.kt new file mode 100644 index 00000000..1b2be2b1 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/log/record/SendArticleEventHistoryRecord.kt @@ -0,0 +1,9 @@ +package com.few.api.repo.dao.log.record + +data class SendArticleEventHistoryRecord( + val memberId: Long, + val articleId: Long, + val messageId: String, + val eventType: Byte, + val sendType: Byte, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/ArticleLogService.kt b/api/src/main/kotlin/com/few/api/domain/article/service/ArticleLogService.kt new file mode 100644 index 00000000..d60d8660 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/service/ArticleLogService.kt @@ -0,0 +1,36 @@ +package com.few.api.domain.article.service + +import com.few.api.domain.article.service.dto.InsertOpenEventDto +import com.few.api.domain.article.service.dto.SelectDeliveryEventByMessageIdDto +import com.few.api.repo.dao.log.SendArticleEventHistoryDao +import com.few.api.repo.dao.log.command.InsertEventCommand +import com.few.api.repo.dao.log.query.SelectEventByMessageIdAndEventTypeQuery +import com.few.api.repo.dao.log.record.SendArticleEventHistoryRecord +import org.springframework.stereotype.Service + +@Service +class ArticleLogService( + private val sendArticleEventHistoryDao: SendArticleEventHistoryDao, +) { + + fun selectDeliveryEventByMessageId(dto: SelectDeliveryEventByMessageIdDto): SendArticleEventHistoryRecord? { + return sendArticleEventHistoryDao.selectEventByMessageId( + SelectEventByMessageIdAndEventTypeQuery( + dto.messageId, + dto.eventType + ) + ) + } + + fun insertOpenEvent(dto: InsertOpenEventDto) { + sendArticleEventHistoryDao.insertEvent( + InsertEventCommand( + memberId = dto.memberId, + articleId = dto.articleId, + messageId = dto.messageId, + eventType = dto.eventType, + sendType = dto.sendType + ) + ) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/ArticleMemberService.kt b/api/src/main/kotlin/com/few/api/domain/article/service/ArticleMemberService.kt new file mode 100644 index 00000000..fc940f09 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/service/ArticleMemberService.kt @@ -0,0 +1,18 @@ +package com.few.api.domain.article.service + +import com.few.api.domain.article.service.dto.ReadMemberByEmailDto +import com.few.api.repo.dao.member.MemberDao +import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery +import org.springframework.stereotype.Service + +@Service +class ArticleMemberService( + private val memberDao: MemberDao, +) { + + fun readMemberByEmail(dto: ReadMemberByEmailDto): Long? { + return memberDao.selectMemberByEmail( + SelectMemberByEmailQuery(dto.email) + )?.memberId + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/dto/InsertOpenEventDto.kt b/api/src/main/kotlin/com/few/api/domain/article/service/dto/InsertOpenEventDto.kt new file mode 100644 index 00000000..c5182918 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/service/dto/InsertOpenEventDto.kt @@ -0,0 +1,9 @@ +package com.few.api.domain.article.service.dto + +data class InsertOpenEventDto( + val memberId: Long, + val articleId: Long, + val messageId: String, + val eventType: Byte, + val sendType: Byte, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadMemberByEmailDto.kt b/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadMemberByEmailDto.kt new file mode 100644 index 00000000..5f2fd660 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadMemberByEmailDto.kt @@ -0,0 +1,5 @@ +package com.few.api.domain.article.service.dto + +data class ReadMemberByEmailDto( + val email: String, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/dto/SelectDeliveryEventByMessageIdDto.kt b/api/src/main/kotlin/com/few/api/domain/article/service/dto/SelectDeliveryEventByMessageIdDto.kt new file mode 100644 index 00000000..88e9d808 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/service/dto/SelectDeliveryEventByMessageIdDto.kt @@ -0,0 +1,6 @@ +package com.few.api.domain.article.service.dto + +data class SelectDeliveryEventByMessageIdDto( + val messageId: String, + val eventType: Byte, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt new file mode 100644 index 00000000..c594b3f9 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt @@ -0,0 +1,45 @@ +package com.few.api.domain.article.usecase + +import com.few.api.domain.article.service.ArticleLogService +import com.few.api.domain.article.service.ArticleMemberService +import com.few.api.domain.article.service.dto.InsertOpenEventDto +import com.few.api.domain.article.service.dto.ReadMemberByEmailDto +import com.few.api.domain.article.service.dto.SelectDeliveryEventByMessageIdDto +import com.few.api.domain.article.usecase.dto.ReadArticleByEmailUseCaseIn +import com.few.api.web.support.EmailLogEventType +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import org.webjars.NotFoundException + +@Component +class ReadArticleByEmailUseCase( + private val memberService: ArticleMemberService, + private val articleLogService: ArticleLogService, +) { + + @Transactional + fun execute(useCaseIn: ReadArticleByEmailUseCaseIn) { + val memberId = + memberService.readMemberByEmail(ReadMemberByEmailDto(useCaseIn.destination[0])) + ?: throw NotFoundException("member.notfound.email") + + val record = + articleLogService.selectDeliveryEventByMessageId( + SelectDeliveryEventByMessageIdDto( + useCaseIn.messageId, + EmailLogEventType.DELIVERY.code + ) + ) + ?: throw IllegalStateException("event is not found") + + articleLogService.insertOpenEvent( + InsertOpenEventDto( + memberId = memberId, + articleId = record.articleId, + messageId = record.messageId, + eventType = EmailLogEventType.OPEN.code, + sendType = record.sendType + ) + ) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleByEmailUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleByEmailUseCaseIn.kt new file mode 100644 index 00000000..4e16d2d4 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleByEmailUseCaseIn.kt @@ -0,0 +1,11 @@ +package com.few.api.domain.article.usecase.dto + +import com.few.api.web.support.EmailLogEventType +import com.few.api.web.support.SendType + +data class ReadArticleByEmailUseCaseIn( + val messageId: String, + val destination: List, + val eventType: EmailLogEventType, + val sendType: SendType, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/log/AddEmailLogUseCase.kt b/api/src/main/kotlin/com/few/api/domain/log/AddEmailLogUseCase.kt new file mode 100644 index 00000000..a6982c2a --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/log/AddEmailLogUseCase.kt @@ -0,0 +1,46 @@ +package com.few.api.domain.log + +import com.few.api.domain.log.dto.AddEmailLogUseCaseIn +import com.few.api.repo.dao.log.SendArticleEventHistoryDao +import com.few.api.repo.dao.log.command.InsertEventCommand +import com.few.api.repo.dao.log.query.SelectEventByMessageIdAndEventTypeQuery +import com.few.api.repo.dao.member.MemberDao +import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery +import com.few.api.web.support.EmailLogEventType +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import org.webjars.NotFoundException + +@Component +class AddEmailLogUseCase( + private val memberDao: MemberDao, + private val sendArticleEventHistoryDao: SendArticleEventHistoryDao, +) { + @Transactional + fun execute(useCaseIn: AddEmailLogUseCaseIn) { + val (memberId, _, _, _) = memberDao.selectMemberByEmail( + SelectMemberByEmailQuery(useCaseIn.destination[0]) + ) ?: throw NotFoundException("member.notfound.email") + + val record = + sendArticleEventHistoryDao.selectEventByMessageId( + SelectEventByMessageIdAndEventTypeQuery( + useCaseIn.messageId, + EmailLogEventType.SEND.code + ) + ) + ?: throw IllegalStateException("event is not found") + + sendArticleEventHistoryDao.insertEvent( + InsertEventCommand( + memberId = memberId, + articleId = record.articleId, + messageId = record.messageId, + eventType = useCaseIn.eventType.code, + sendType = record.sendType + ) + ).let { +// TODO("다른 이벤트는 필요시 추가한다.") + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/log/dto/AddEmailLogUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/log/dto/AddEmailLogUseCaseIn.kt new file mode 100644 index 00000000..bab41880 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/log/dto/AddEmailLogUseCaseIn.kt @@ -0,0 +1,11 @@ +package com.few.api.domain.log.dto + +import com.few.api.web.support.EmailLogEventType +import java.time.LocalDateTime + +data class AddEmailLogUseCaseIn( + val eventType: EmailLogEventType, + val messageId: String, + val destination: List, + val mailTimestamp: LocalDateTime, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt b/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt index 9bf3d27c..99bf96e1 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt @@ -3,16 +3,14 @@ package com.few.api.domain.subscription.handler import com.few.api.config.DatabaseAccessThreadPoolConfig.Companion.DATABASE_ACCESS_POOL import com.few.api.domain.common.lock.LockFor import com.few.api.domain.common.lock.LockIdentifier -import com.few.api.domain.subscription.service.SubscriptionArticleService -import com.few.api.domain.subscription.service.SubscriptionMemberService -import com.few.api.domain.subscription.service.SubscriptionEmailService -import com.few.api.domain.subscription.service.SubscriptionWorkbookService +import com.few.api.domain.subscription.service.* import com.few.api.domain.subscription.service.dto.* import com.few.api.exception.common.NotFoundException import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.UpdateArticleProgressCommand import com.few.api.repo.dao.subscription.command.UpdateLastArticleProgressCommand import com.few.api.repo.dao.subscription.query.SelectSubscriptionQuery +import com.few.api.web.support.SendType import com.few.data.common.code.CategoryType import com.few.email.service.article.dto.Content import io.github.oshai.kotlinlogging.KotlinLogging @@ -27,6 +25,7 @@ class SendWorkbookArticleAsyncHandler( private val memberService: SubscriptionMemberService, private val articleService: SubscriptionArticleService, private val workbookService: SubscriptionWorkbookService, + private val subscriptionLogService: SubscriptionLogService, private val subscriptionDao: SubscriptionDao, private val emailService: SubscriptionEmailService, ) { @@ -80,8 +79,19 @@ class SendWorkbookArticleAsyncHandler( ) ) - runCatching { emailService.sendArticleEmail(sendArticleInDto) } + runCatching { + emailService.sendArticleEmail(sendArticleInDto) + } .onSuccess { + subscriptionLogService.insertSendEvent( + InsertSendEventDto( + memberId = memberId, + articleId = article.id, + messageId = it, + sendType = SendType.AWSSES.code + ) + ) + val lastDayArticleId = workbookService.readWorkbookLastArticleId( ReadWorkbookLastArticleIdInDto( diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt index 2865ed18..311ef73a 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt @@ -15,8 +15,8 @@ class SubscriptionEmailService( private const val ARTICLE_TEMPLATE = "article" } - fun sendArticleEmail(dto: SendArticleInDto) { - sendArticleEmailService.send( + fun sendArticleEmail(dto: SendArticleInDto): String { + return sendArticleEmailService.send( SendArticleEmailArgs( dto.toEmail, ARTICLE_SUBJECT_TEMPLATE.format( diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionLogService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionLogService.kt new file mode 100644 index 00000000..858546fc --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionLogService.kt @@ -0,0 +1,25 @@ +package com.few.api.domain.subscription.service + +import com.few.api.domain.subscription.service.dto.InsertSendEventDto +import com.few.api.repo.dao.log.SendArticleEventHistoryDao +import com.few.api.repo.dao.log.command.InsertEventCommand +import com.few.api.web.support.EmailLogEventType +import org.springframework.stereotype.Service + +@Service +class SubscriptionLogService( + private val sendArticleEventHistoryDao: SendArticleEventHistoryDao, +) { + + fun insertSendEvent(dto: InsertSendEventDto) { + sendArticleEventHistoryDao.insertEvent( + InsertEventCommand( + memberId = dto.memberId, + articleId = dto.articleId, + messageId = dto.messageId, + eventType = EmailLogEventType.SEND.code, + sendType = dto.sendType + ) + ) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/dto/InsertSendEventDto.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/dto/InsertSendEventDto.kt new file mode 100644 index 00000000..ec773607 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/dto/InsertSendEventDto.kt @@ -0,0 +1,8 @@ +package com.few.api.domain.subscription.service.dto + +data class InsertSendEventDto( + val memberId: Long, + val articleId: Long, + val messageId: String, + val sendType: Byte, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt b/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt index 58f303b9..04161216 100644 --- a/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt +++ b/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt @@ -144,7 +144,9 @@ class WebSecurityConfig( /** 어드민 */ AntPathRequestMatcher("/api/v1/admin/**", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/articles/views", HttpMethod.POST.name()), AntPathRequestMatcher("/api/v1/logs", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/logs/email/articles", HttpMethod.POST.name()), AntPathRequestMatcher("/batch/**"), /** 인증 불필요 */ @@ -188,6 +190,8 @@ class WebSecurityConfig( /** 어드민 */ AntPathRequestMatcher("/api/v1/admin/**", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/articles/views", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/logs/email/articles", HttpMethod.POST.name()), AntPathRequestMatcher("/api/v1/logs", HttpMethod.POST.name()), AntPathRequestMatcher("/batch/**"), diff --git a/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt index a9b17fca..aec76da8 100644 --- a/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt +++ b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt @@ -1,8 +1,6 @@ package com.few.api.web.config -import com.few.api.web.config.converter.DayCodeConverter -import com.few.api.web.config.converter.ViewConverter -import com.few.api.web.config.converter.WorkBookCategoryConverter +import com.few.api.web.config.converter.* import com.few.api.web.support.method.UserArgumentHandlerMethodArgumentResolver import org.springframework.context.annotation.Configuration import org.springframework.format.FormatterRegistry @@ -34,6 +32,8 @@ class WebConfig( registry.addConverter(WorkBookCategoryConverter()) registry.addConverter(ViewConverter()) registry.addConverter(DayCodeConverter()) + registry.addConverter(EmailLogEventTypeConverter()) + registry.addConverter(SendTypeConverter()) } override fun addArgumentResolvers(argumentResolvers: MutableList) { diff --git a/api/src/main/kotlin/com/few/api/web/config/converter/EmailLogEventTypeConverter.kt b/api/src/main/kotlin/com/few/api/web/config/converter/EmailLogEventTypeConverter.kt new file mode 100644 index 00000000..ae8aa410 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/config/converter/EmailLogEventTypeConverter.kt @@ -0,0 +1,11 @@ +package com.few.api.web.config.converter + +import com.few.api.web.support.EmailLogEventType +import org.springframework.core.convert.converter.Converter + +class EmailLogEventTypeConverter : Converter { + override fun convert(source: String): EmailLogEventType { + return EmailLogEventType.fromType(source) + ?: throw IllegalArgumentException("EmailLogEventType not found. type=$source") + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/config/converter/SendTypeConverter.kt b/api/src/main/kotlin/com/few/api/web/config/converter/SendTypeConverter.kt new file mode 100644 index 00000000..ae84a0f0 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/config/converter/SendTypeConverter.kt @@ -0,0 +1,10 @@ +package com.few.api.web.config.converter + +import com.few.api.web.support.SendType +import org.springframework.core.convert.converter.Converter + +class SendTypeConverter : Converter { + override fun convert(source: String): SendType { + return SendType.fromCode(source.toByte()) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/admin/ApiLogController.kt b/api/src/main/kotlin/com/few/api/web/controller/admin/ApiLogController.kt index 11777273..58824e9b 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/admin/ApiLogController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/admin/ApiLogController.kt @@ -1,10 +1,13 @@ package com.few.api.web.controller.admin import com.few.api.domain.log.AddApiLogUseCase +import com.few.api.domain.log.AddEmailLogUseCase import com.few.api.domain.log.dto.AddApiLogUseCaseIn +import com.few.api.domain.log.dto.AddEmailLogUseCaseIn import com.few.api.web.controller.admin.request.* import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator +import com.few.api.web.support.EmailLogEventType import org.springframework.http.HttpStatus import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.PostMapping @@ -17,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping(value = ["/api/v1/logs"]) class ApiLogController( private val addApiLogUseCase: AddApiLogUseCase, + private val addEmailLogUseCase: AddEmailLogUseCase, ) { @PostMapping fun addApiLog(@RequestBody request: ApiLogRequest): ApiResponse { @@ -26,4 +30,18 @@ class ApiLogController( return ApiResponseGenerator.success(HttpStatus.OK) } + + @PostMapping("/email/articles") + fun addEmailLog(@RequestBody request: EmailLogRequest): ApiResponse { + AddEmailLogUseCaseIn( + eventType = EmailLogEventType.fromType(request.eventType) + ?: throw IllegalArgumentException("EmailLogEventType not found. type=${request.eventType}"), + messageId = request.messageId, + destination = request.destination, + mailTimestamp = request.mailTimestamp + ).let { + addEmailLogUseCase.execute(it) + } + return ApiResponseGenerator.success(HttpStatus.OK) + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/admin/request/EmailLogRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/admin/request/EmailLogRequest.kt new file mode 100644 index 00000000..ed46a2f3 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/controller/admin/request/EmailLogRequest.kt @@ -0,0 +1,12 @@ +package com.few.api.web.controller.admin.request + +import com.fasterxml.jackson.annotation.JsonFormat +import java.time.LocalDateTime + +data class EmailLogRequest( + val eventType: String, + val messageId: String, + val destination: List, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + val mailTimestamp: LocalDateTime, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt index ecb4d6f0..ccbf3788 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt @@ -2,14 +2,19 @@ package com.few.api.web.controller.article import com.few.api.domain.article.usecase.ReadArticleUseCase import com.few.api.domain.article.usecase.BrowseArticlesUseCase +import com.few.api.domain.article.usecase.ReadArticleByEmailUseCase +import com.few.api.domain.article.usecase.dto.ReadArticleByEmailUseCaseIn import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseIn +import com.few.api.web.controller.article.request.ReadArticleByEmailRequest import com.few.api.web.controller.article.response.ReadArticleResponse import com.few.api.web.controller.article.response.ReadArticlesResponse import com.few.api.web.controller.article.response.WorkbookInfo import com.few.api.web.controller.article.response.WriterInfo import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator +import com.few.api.web.support.EmailLogEventType +import com.few.api.web.support.SendType import com.few.api.web.support.method.UserArgument import com.few.api.web.support.method.UserArgumentDetails import com.few.data.common.code.CategoryType @@ -25,6 +30,7 @@ import org.springframework.web.bind.annotation.* class ArticleController( private val readArticleUseCase: ReadArticleUseCase, private val browseArticlesUseCase: BrowseArticlesUseCase, + private val readArticleByEmailUseCase: ReadArticleByEmailUseCase, ) { @GetMapping("/{articleId}") @@ -74,7 +80,8 @@ class ArticleController( defaultValue = "-1" ) categoryCd: Byte, ): ApiResponse> { - val useCaseOut = browseArticlesUseCase.execute(ReadArticlesUseCaseIn(prevArticleId, categoryCd)) + val useCaseOut = + browseArticlesUseCase.execute(ReadArticlesUseCaseIn(prevArticleId, categoryCd)) val articles: List = useCaseOut.articles.map { a -> ReadArticleResponse( @@ -115,4 +122,20 @@ class ArticleController( HttpStatus.OK ) } + + @PostMapping("/views") + fun readArticleByEmail( + @RequestParam("type") type: SendType, + @RequestBody request: ReadArticleByEmailRequest, + ): ApiResponse { + readArticleByEmailUseCase.execute( + ReadArticleByEmailUseCaseIn( + destination = request.destination, + messageId = request.messageId, + eventType = EmailLogEventType.OPEN, + sendType = type + ) + ) + return ApiResponseGenerator.success(HttpStatus.OK) + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/article/request/ReadArticleByEmailRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/article/request/ReadArticleByEmailRequest.kt new file mode 100644 index 00000000..10da32ec --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/controller/article/request/ReadArticleByEmailRequest.kt @@ -0,0 +1,6 @@ +package com.few.api.web.controller.article.request + +data class ReadArticleByEmailRequest( + val messageId: String, + val destination: List, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/EmailLogEventType.kt b/api/src/main/kotlin/com/few/api/web/support/EmailLogEventType.kt new file mode 100644 index 00000000..7628cd23 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/EmailLogEventType.kt @@ -0,0 +1,23 @@ +package com.few.api.web.support + +/** + * @see com.few.data.common.code.SendEventType + */ +enum class EmailLogEventType(val code: Byte, val type: String) { + OPEN(0, "open"), + DELIVERY(1, "delivery"), + CLICK(2, "click"), + SEND(3, "send"), + DELIVERYDELAY(4, "deliverydelay"), + ; + + companion object { + fun fromType(type: String): EmailLogEventType? { + return entries.find { it.type == type.lowercase() } + } + + fun fromCode(code: Byte): EmailLogEventType? { + return entries.find { it.code == code } + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/SendType.kt b/api/src/main/kotlin/com/few/api/web/support/SendType.kt new file mode 100644 index 00000000..e4f61bfe --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/SendType.kt @@ -0,0 +1,16 @@ +package com.few.api.web.support + +/** + * @see com.few.data.common.code.SendType + */ +enum class SendType(val code: Byte) { + EMAIL(0), + AWSSES(1), + ; + + companion object { + fun fromCode(code: Byte): SendType { + return entries.first { it.code == code } + } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendEventType.kt b/batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendEventType.kt new file mode 100644 index 00000000..e42be88c --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendEventType.kt @@ -0,0 +1,24 @@ +package com.few.batch.data.common.code + +/** + * @see com.few.data.common.code.SendEventType + */ +enum class BatchSendEventType(val code: Byte, val type: String) { + + OPEN(0, "open"), + DELIVERY(1, "delivery"), + CLICK(2, "click"), + SEND(3, "send"), + DELIVERYDELAY(4, "deliverydelay"), + ; + + companion object { + fun fromType(type: String): BatchSendEventType? { + return entries.find { it.type == type } + } + + fun fromCode(code: Byte): BatchSendEventType? { + return entries.find { it.code == code } + } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendType.kt b/batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendType.kt new file mode 100644 index 00000000..dd85d8e7 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/data/common/code/BatchSendType.kt @@ -0,0 +1,9 @@ +package com.few.batch.data.common.code + +/** + * @see com.few.data.common.code.SendType + */ +enum class BatchSendType(val code: Byte) { + EMAIL(0), + AWSSES(1), +} \ No newline at end of file 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 76b340e1..596d1135 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,5 +1,7 @@ package com.few.batch.service.article.writer +import com.few.batch.data.common.code.BatchSendEventType +import com.few.batch.data.common.code.BatchSendType import com.few.batch.service.article.dto.WorkBookSubscriberItem import com.few.batch.service.article.dto.toMemberIds import com.few.batch.service.article.dto.toTargetWorkBookIds @@ -11,6 +13,7 @@ import com.few.batch.service.article.writer.support.MailServiceArgsGenerator import com.few.email.service.article.SendArticleEmailService import jooq.jooq_dsl.tables.* import org.jooq.DSLContext +import org.jooq.InsertSetMoreStep import org.jooq.UpdateConditionStep import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -56,9 +59,18 @@ class WorkBookSubscriberWriter( /** 이메일 전송 */ val mailSendRecorder = MailSendRecorder(emailServiceArgs) + val insertSendArticleQueries = mutableListOf>() emailServiceArgs.forEach { try { - sendArticleEmailService.send(it.sendArticleEmailArgs) + val messageId = sendArticleEmailService.send(it.sendArticleEmailArgs) + insertSendArticleQueries.add( + dslContext.insertInto(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MEMBER_ID, it.memberId) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.ARTICLE_ID, it.articleId) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.MESSAGE_ID, messageId) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.EVENT_TYPE_CD, BatchSendEventType.SEND.code) + .set(SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY.SEND_TYPE_CD, BatchSendType.AWSSES.code) + ) } catch (e: Exception) { mailSendRecorder.recordFail( it.memberId, @@ -67,6 +79,7 @@ class WorkBookSubscriberWriter( ) } } + dslContext.batch(insertSendArticleQueries).execute() /** 이메일 전송 결과에 따라 진행률 업데이트 및 구독 해지 처리를 위한 데이터 생성 */ val receiveLastDayRecords = 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 index 4ca59c10..8c4a1089 100644 --- 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 @@ -13,6 +13,7 @@ import java.time.LocalDate data class MailServiceArg( val memberId: Long, val workbookId: Long, + val articleId: Long, val sendArticleEmailArgs: SendArticleEmailArgs, ) @@ -53,7 +54,8 @@ class MailServiceArgsGenerator( MailServiceArg( it.memberId, - it.targetWorkBookId, + memberArticle.workbookId, + memberArticle.articleId, SendArticleEmailArgs( toEmail, ARTICLE_SUBJECT_TEMPLATE.format( diff --git a/data/db/migration/entity/V1.00.0.25__add_send_article_event_history_table.sql b/data/db/migration/entity/V1.00.0.25__add_send_article_event_history_table.sql new file mode 100644 index 00000000..08414121 --- /dev/null +++ b/data/db/migration/entity/V1.00.0.25__add_send_article_event_history_table.sql @@ -0,0 +1,12 @@ +-- 전송 아티클 이벤트 히스토리 테이블 +CREATE TABLE SEND_ARTICLE_EVENT_HISTORY +( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + article_id BIGINT NOT NULL, + message_id VARCHAR(255) NOT NULL, + event_type_cd TINYINT NOT NULL, + send_type_cd TINYINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); diff --git a/data/src/main/kotlin/com/few/data/common/code/SendEventType.kt b/data/src/main/kotlin/com/few/data/common/code/SendEventType.kt new file mode 100644 index 00000000..6daf4b3e --- /dev/null +++ b/data/src/main/kotlin/com/few/data/common/code/SendEventType.kt @@ -0,0 +1,24 @@ +package com.few.data.common.code + +/** + * @see com.few.api.web.support.EmailLogEventType + * @see com.few.batch.data.common.code.BatchSendEventType + */ +enum class SendEventType(val code: Byte, val type: String) { + OPEN(0, "open"), + DELIVERY(1, "delivery"), + CLICK(2, "click"), + SEND(3, "send"), + DELIVERYDELAY(4, "deliverydelay"), + ; + + companion object { + fun fromType(type: String): SendEventType? { + return entries.find { it.type == type } + } + + fun fromCode(code: Byte): SendEventType? { + return entries.find { it.code == code } + } + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/com/few/data/common/code/SendType.kt b/data/src/main/kotlin/com/few/data/common/code/SendType.kt new file mode 100644 index 00000000..7ec20a82 --- /dev/null +++ b/data/src/main/kotlin/com/few/data/common/code/SendType.kt @@ -0,0 +1,10 @@ +package com.few.data.common.code + +/** + * @see com.few.api.web.support.SendType + * @see com.few.batch.data.common.code.BatchSendType + */ +enum class SendType(val code: Byte) { + EMAIL(0), + AWSSES(1), +} \ No newline at end of file