Skip to content

Commit

Permalink
[Feat/#139] 구독시 디스코드 훅 / 신규 구독 안되는 문제 해결 (#140)
Browse files Browse the repository at this point in the history
* fix: 새로운 구독이 되지 않는 문제 해결

* feat: api/client 패키지 구성 추가

* feat: ClientConfig 구현

* feat: DiscordBodyProperty 추가

* feat: announceWorkbookSubscription 구현

* feat: @EnableAsync 활성화

* feat: countAllSubscriptionStatus 구현

* feat: readWorkbookTitle 구현

* feat: WorkbookSubscriptionEvent 구현

* feat: WorkbookSubscriptionEvent 발행 추가

* feat: 테스트에 추가된 api/client 구성 추가

* refactor: WorkbookSubscriptionStatus 필드 명 수정

- id -> workbookId
- subHistory -> isActiveSub

* refactor: selectAllWorkbookSubscriptionStatus -> selectTopWorkbookSubscriptionStatus 수정

* refactor: SubscribeWorkbookUseCase 구독/재구독 로직 수정

* feat: 디스코드 훅 전송 응답 로그 추가
  • Loading branch information
belljun3395 authored Jul 8, 2024
1 parent 6a73076 commit 606b325
Show file tree
Hide file tree
Showing 17 changed files with 242 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInWorkbookSubscr
import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStatusQueryNotConsiderDeletedAt
import com.few.api.repo.dao.subscription.record.WorkbookSubscriptionStatus
import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery
import com.few.api.repo.dao.subscription.record.CountAllSubscriptionStatusRecord
import jooq.jooq_dsl.Tables.MAPPING_WORKBOOK_ARTICLE
import jooq.jooq_dsl.Tables.SUBSCRIPTION
import org.jooq.DSLContext
Expand Down Expand Up @@ -43,16 +44,18 @@ class SubscriptionDao(
.execute()
}

fun selectAllWorkbookSubscriptionStatus(query: SelectAllWorkbookSubscriptionStatusQueryNotConsiderDeletedAt): List<WorkbookSubscriptionStatus> {
fun selectTopWorkbookSubscriptionStatus(query: SelectAllWorkbookSubscriptionStatusQueryNotConsiderDeletedAt): WorkbookSubscriptionStatus? {
return dslContext.select(
SUBSCRIPTION.ID.`as`(WorkbookSubscriptionStatus::id.name),
SUBSCRIPTION.DELETED_AT.isNotNull.`as`(WorkbookSubscriptionStatus::subHistory.name),
SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(WorkbookSubscriptionStatus::workbookId.name),
SUBSCRIPTION.DELETED_AT.isNull.`as`(WorkbookSubscriptionStatus::isActiveSub.name),
SUBSCRIPTION.PROGRESS.add(1).`as`(WorkbookSubscriptionStatus::day.name)
)
.from(SUBSCRIPTION)
.where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId))
.and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(query.workbookId))
.fetchInto(WorkbookSubscriptionStatus::class.java)
.orderBy(SUBSCRIPTION.CREATED_AT.desc())
.limit(1)
.fetchOneInto(WorkbookSubscriptionStatus::class.java)
}

fun updateDeletedAtInAllSubscription(command: UpdateDeletedAtInAllSubscriptionCommand) {
Expand All @@ -69,4 +72,15 @@ class SubscriptionDao(
.where(MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(query.workbookId))
.fetchOne(0, Int::class.java)
}

fun countAllSubscriptionStatus(): CountAllSubscriptionStatusRecord {
val total = dslContext.selectCount()
.from(SUBSCRIPTION)
.fetchOne(0, Int::class.java)!!
val active = dslContext.selectCount()
.from(SUBSCRIPTION)
.where(SUBSCRIPTION.DELETED_AT.isNull)
.fetchOne(0, Int::class.java)!!
return CountAllSubscriptionStatusRecord(total.toLong(), active.toLong())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.repo.dao.subscription.record

data class CountAllSubscriptionStatusRecord(
val totalSubscriptions: Long,
val activeSubscriptions: Long
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.few.api.repo.dao.subscription.record

data class WorkbookSubscriptionStatus(
val id: Long,
val subHistory: Boolean,
val workbookId: Long,
val isActiveSub: Boolean,
val day: Int
)
24 changes: 24 additions & 0 deletions api/src/main/kotlin/com/few/api/client/config/ClientConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.few.api.client.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.web.client.RestTemplateBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestTemplate
import java.time.Duration

@Configuration
class ClientConfig {

@Bean
fun restTemplate(
restTemplateBuilder: RestTemplateBuilder,
@Value("\${client.timeout.connect}") connectTimeout: Int,
@Value("\${client.timeout.read}") readTimeout: Int
): RestTemplate {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(connectTimeout.toLong()))
.setReadTimeout(Duration.ofSeconds(readTimeout.toLong()))
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.few.api.client.config.properties

data class DiscordBodyProperty(
val content: String,
val embeds: List<Embed>
)

data class Embed(
val title: String,
val description: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.few.api.client.subscription

import com.few.api.client.config.properties.DiscordBodyProperty
import com.few.api.client.config.properties.Embed
import com.few.api.client.subscription.dto.WorkbookSubscriptionArgs
import org.apache.juli.logging.LogFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate

@Service
class SubscriptionClient(
private val restTemplate: RestTemplate,
@Value("\${webhook.discord}") private val discordWebhook: String
) {
private val log = LogFactory.getLog(SubscriptionClient::class.java)

fun announceWorkbookSubscription(args: WorkbookSubscriptionArgs) {
args.let {
DiscordBodyProperty(
content = "🎉 신규 구독 알림 ",
embeds = listOf(
Embed(
title = "Total Subscriptions",
description = it.totalSubscriptions.toString()
),
Embed(
title = "Active Subscriptions",
description = it.activeSubscriptions.toString()
),
Embed(
title = "Workbook Title",
description = it.workbookTitle
)
)
)
}.let { body ->
restTemplate.exchange(
discordWebhook,
HttpMethod.POST,
HttpEntity(body),
String::class.java
).let { res ->
log.info("Discord webhook response: ${res.statusCode}")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.few.api.client.subscription.dto

data class WorkbookSubscriptionArgs(
val totalSubscriptions: Long,
val activeSubscriptions: Long,
val workbookTitle: String
)
2 changes: 2 additions & 0 deletions api/src/main/kotlin/com/few/api/config/ApiConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import com.few.storage.image.config.ImageStorageConfig
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.web.servlet.config.annotation.EnableWebMvc

@Configuration
@ComponentScan(basePackages = [ApiConfig.BASE_PACKAGE])
@Import(ApiRepoConfig::class, BatchConfig::class, ImageStorageConfig::class, DocumentStorageConfig::class)
@EnableWebMvc
@EnableAsync
class ApiConfig {
companion object {
const val BASE_PACKAGE = "com.few.api"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.few.api.domain.subscription.event

import com.few.api.client.subscription.SubscriptionClient
import com.few.api.client.subscription.dto.WorkbookSubscriptionArgs
import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent
import com.few.api.domain.subscription.service.WorkbookService
import com.few.api.domain.subscription.service.dto.ReadWorkbookTitleDto
import com.few.api.repo.dao.subscription.SubscriptionDao
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component

@Component
class WorkbookSubscriptionEventListener(
private val subscriptionDao: SubscriptionDao,
private val subscriptionClient: SubscriptionClient,
private val workbookService: WorkbookService
) {

@Async
@EventListener
fun handleWorkbookSubscriptionEvent(event: WorkbookSubscriptionEvent) {
val title = ReadWorkbookTitleDto(event.workbookId).let { dto ->
workbookService.readWorkbookTitle(dto)
?: throw RuntimeException("Workbook not found")
}
subscriptionDao.countAllSubscriptionStatus().let { record ->
WorkbookSubscriptionArgs(
totalSubscriptions = record.totalSubscriptions,
activeSubscriptions = record.activeSubscriptions,
workbookTitle = title
).let { args ->
subscriptionClient.announceWorkbookSubscription(args)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.domain.subscription.event.dto

data class WorkbookSubscriptionEvent(
val workbookId: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.few.api.domain.subscription.service

import com.few.api.domain.subscription.service.dto.ReadWorkbookTitleDto
import com.few.api.repo.dao.workbook.WorkbookDao
import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery
import org.springframework.stereotype.Service

@Service
class WorkbookService(
private val workbookDao: WorkbookDao
) {

fun readWorkbookTitle(dto: ReadWorkbookTitleDto): String? {
return SelectWorkBookRecordQuery(dto.workbookId).let { query ->
workbookDao.selectWorkBook(query)?.title
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.domain.subscription.service.dto

data class ReadWorkbookTitleDto(
val workbookId: Long
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.few.api.domain.subscription.usecase

import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent
import com.few.api.domain.subscription.service.MemberService
import com.few.api.domain.subscription.service.dto.InsertMemberDto
import com.few.api.domain.subscription.service.dto.ReadMemberIdDto
Expand All @@ -9,16 +10,17 @@ import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStat
import com.few.api.domain.subscription.usecase.`in`.SubscribeWorkbookUseCaseIn
import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery
import com.few.data.common.code.MemberType
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class SubscribeWorkbookUseCase(
private val subscriptionDao: SubscriptionDao,
private val memberService: MemberService
private val memberService: MemberService,
private val applicationEventPublisher: ApplicationEventPublisher
) {

// todo 이미 가입된 경우
@Transactional
fun execute(useCaseIn: SubscribeWorkbookUseCaseIn) {
// TODO: request sending email
Expand All @@ -28,29 +30,36 @@ class SubscribeWorkbookUseCase(
)

val subTargetWorkbookId = useCaseIn.workbookId
SelectAllWorkbookSubscriptionStatusQueryNotConsiderDeletedAt(memberId = memberId, workbookId = subTargetWorkbookId).let { query ->
subscriptionDao.selectAllWorkbookSubscriptionStatus(query).let { subscriptionStatusList ->
if (subscriptionStatusList.isNotEmpty()) {
subscriptionStatusList.stream().filter { status ->
status.id == query.workbookId
}.findAny().get().let { status ->
InsertWorkbookSubscriptionCommand(memberId = memberId, workbookId = subTargetWorkbookId).let { command ->
if (status.subHistory) {
CountWorkbookMappedArticlesQuery(subTargetWorkbookId).let { query ->
subscriptionDao.countWorkbookMappedArticles(query)
}?.let { lastDay ->
if (lastDay <= (status.day)) {
throw RuntimeException("이미 학습을 완료한 워크북입니다.")
}
subscriptionDao.reSubscribeWorkbookSubscription(command)
}
} else {
subscriptionDao.insertWorkbookSubscription(command)
}
}
}
val command = InsertWorkbookSubscriptionCommand(
memberId = memberId,
workbookId = subTargetWorkbookId
)

val subscriptionStatus = subscriptionDao.selectTopWorkbookSubscriptionStatus(
SelectAllWorkbookSubscriptionStatusQueryNotConsiderDeletedAt(memberId = memberId, workbookId = subTargetWorkbookId)
)

when {
/** 구독한 히스토리가 없는 경우 */
subscriptionStatus == null -> {
subscriptionDao.insertWorkbookSubscription(command)
}

/** 이미 구독한 히스토리가 있고 구독이 취소된 경우 */
!subscriptionStatus.isActiveSub -> {
val lastDay = subscriptionDao.countWorkbookMappedArticles(CountWorkbookMappedArticlesQuery(subTargetWorkbookId)) ?: throw RuntimeException("워크북 매핑된 아티클을 조회할 수 없습니다.")
if (lastDay <= subscriptionStatus.day) {
throw RuntimeException("이미 학습을 완료한 워크북입니다.")
}
/** 재구독 */
subscriptionDao.reSubscribeWorkbookSubscription(command)
}

/** 이미 구독한 히스토리가 있고 구독이 취소되지 않은 경우 */
else -> {
throw RuntimeException("이미 구독한 워크북입니다.")
}
}
applicationEventPublisher.publishEvent(WorkbookSubscriptionEvent(workbookId = subTargetWorkbookId))
}
}
7 changes: 7 additions & 0 deletions api/src/main/resources/application-client-local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
client:
timeout:
connect: 5000
read: 5000

webhook:
discord: ${WEBHOOK_DISCORD}
7 changes: 7 additions & 0 deletions api/src/main/resources/application-client-prd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
client:
timeout:
connect: ${TIMEOUT_CONNECT:5000}
read: ${TIMEOUT_READ:5000}

webhook:
discord: ${WEBHOOK_DISCORD}
8 changes: 8 additions & 0 deletions api/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ document:

cdn:
url: http://127.0.0.1:9000

client:
timeout:
connect: 5000
read: 5000

webhook:
discord: https://discord.com/api/webhooks/1234567890/abcdefg
3 changes: 3 additions & 0 deletions resources/docker/docker-compose-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ services:
IMAGE_STORE_BUCKET_NAME: ${IMAGE_STORE_BUCKET_NAME}
DOCUMENT_STORE_BUCKET_NAME: ${DOCUMENT_STORE_BUCKET_NAME}
CDN_URL: ${CDN_URL}
WEBHOOK_DISCORD: ${WEBHOOK_DISCORD}
TIMEOUT_CONNECT: ${TIMEOUT_CONNECT}
TIMEOUT_READ: ${TIMEOUT_READ}

0 comments on commit 606b325

Please sign in to comment.