From f0ec85ad2f74de141b2a0da4f703c4b419cef700 Mon Sep 17 00:00:00 2001 From: DongGeon0908 Date: Tue, 24 Sep 2024 23:31:00 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20discord=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B0=9C=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B3=A0=EB=8F=84?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../batch/statistics/job/HeroStatisticsJob.kt | 10 ++--- .../client/discord/DiscordWebhookService.kt | 8 +++- .../discord/client/DiscordWebhookClient.kt | 5 ++- .../client/SuspendableDiscordWebhookClient.kt | 15 ++++++- .../config/DiscordWebhookClientConfig.kt | 14 +++++- .../dev/application/DevDeleteService.kt | 41 +++++++++++++++++ .../domain/dev/resource/DevDeleteResource.kt | 2 +- .../dev/resource/DevDiscordWebhookResource.kt | 4 +- .../application/DiscussionService.kt | 9 +++- .../event/listener/DiscussionEventListener.kt | 45 +++++++++++++++++++ .../event/listener/WithdrawEventListener.kt | 14 ++---- .../com/hero/alignlab/event/model/Event.kt | 5 +++ .../com/hero/alignlab/exception/ErrorCode.kt | 3 ++ src/main/resources/config/application.yml | 7 ++- 14 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/com/hero/alignlab/event/listener/DiscussionEventListener.kt diff --git a/src/main/kotlin/com/hero/alignlab/batch/statistics/job/HeroStatisticsJob.kt b/src/main/kotlin/com/hero/alignlab/batch/statistics/job/HeroStatisticsJob.kt index 143af84..acf503a 100644 --- a/src/main/kotlin/com/hero/alignlab/batch/statistics/job/HeroStatisticsJob.kt +++ b/src/main/kotlin/com/hero/alignlab/batch/statistics/job/HeroStatisticsJob.kt @@ -1,6 +1,7 @@ package com.hero.alignlab.batch.statistics.job import com.hero.alignlab.client.discord.DiscordWebhookService +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig import com.hero.alignlab.client.discord.model.request.SendMessageRequest import com.hero.alignlab.domain.discussion.infrastructure.DiscussionRepository import com.hero.alignlab.domain.group.infrastructure.GroupRepository @@ -113,10 +114,6 @@ class HeroStatisticsJob( val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - val text = countActiveUserTop3.await().joinToString("\n") { - "- uid: ${it.uid} / count: ${it.count}" - } - val message = """ **$title [${fromDate.format(formatter)} ~ ${toDate.format(formatter)}]** @@ -145,7 +142,10 @@ class HeroStatisticsJob( **이용자 현황** - ${countActiveUserTop3.await().joinToString(" | ") { "uid : ${it.uid} count : ${it.count}" }} """.trimIndent() - discordWebhookService.sendMessage(SendMessageRequest(message)) + discordWebhookService.sendMessage( + channel = DiscordWebhookClientConfig.Config.Channel.STATISTICS, + request = SendMessageRequest(message) + ) } } } diff --git a/src/main/kotlin/com/hero/alignlab/client/discord/DiscordWebhookService.kt b/src/main/kotlin/com/hero/alignlab/client/discord/DiscordWebhookService.kt index 7fcef44..7b6ef1c 100644 --- a/src/main/kotlin/com/hero/alignlab/client/discord/DiscordWebhookService.kt +++ b/src/main/kotlin/com/hero/alignlab/client/discord/DiscordWebhookService.kt @@ -1,6 +1,7 @@ package com.hero.alignlab.client.discord import com.hero.alignlab.client.discord.client.DiscordWebhookClient +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig import com.hero.alignlab.client.discord.model.request.SendMessageRequest import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -10,9 +11,12 @@ import org.springframework.stereotype.Service class DiscordWebhookService( private val discordWebhookClient: DiscordWebhookClient, ) { - suspend fun sendMessage(request: SendMessageRequest) { + suspend fun sendMessage( + channel: DiscordWebhookClientConfig.Config.Channel, + request: SendMessageRequest + ) { withContext(Dispatchers.IO) { - discordWebhookClient.sendMessage(request) + discordWebhookClient.sendMessage(channel, request) } } } diff --git a/src/main/kotlin/com/hero/alignlab/client/discord/client/DiscordWebhookClient.kt b/src/main/kotlin/com/hero/alignlab/client/discord/client/DiscordWebhookClient.kt index 701822d..c65792a 100644 --- a/src/main/kotlin/com/hero/alignlab/client/discord/client/DiscordWebhookClient.kt +++ b/src/main/kotlin/com/hero/alignlab/client/discord/client/DiscordWebhookClient.kt @@ -1,10 +1,11 @@ package com.hero.alignlab.client.discord.client +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig import com.hero.alignlab.client.discord.model.request.SendMessageRequest import com.hero.alignlab.client.discord.model.response.GetWebhookWithTokenResponse interface DiscordWebhookClient { - suspend fun getWebhookWithToken(): GetWebhookWithTokenResponse + suspend fun getWebhookWithToken(channel: DiscordWebhookClientConfig.Config.Channel): GetWebhookWithTokenResponse - suspend fun sendMessage(request: SendMessageRequest) + suspend fun sendMessage(channel: DiscordWebhookClientConfig.Config.Channel, request: SendMessageRequest) } diff --git a/src/main/kotlin/com/hero/alignlab/client/discord/client/SuspendableDiscordWebhookClient.kt b/src/main/kotlin/com/hero/alignlab/client/discord/client/SuspendableDiscordWebhookClient.kt index a1dc625..bfc86ff 100644 --- a/src/main/kotlin/com/hero/alignlab/client/discord/client/SuspendableDiscordWebhookClient.kt +++ b/src/main/kotlin/com/hero/alignlab/client/discord/client/SuspendableDiscordWebhookClient.kt @@ -1,23 +1,34 @@ package com.hero.alignlab.client.discord.client +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig.Config.Channel +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig.Config.Token import com.hero.alignlab.client.discord.model.request.SendMessageRequest import com.hero.alignlab.client.discord.model.response.GetWebhookWithTokenResponse import com.hero.alignlab.client.kakao.SuspendableClient +import com.hero.alignlab.exception.ErrorCode +import com.hero.alignlab.exception.NotFoundException import org.springframework.web.reactive.function.client.WebClient class SuspendableDiscordWebhookClient( client: WebClient, + private val targets: Map, ) : DiscordWebhookClient, SuspendableClient(client) { - override suspend fun getWebhookWithToken(): GetWebhookWithTokenResponse { + override suspend fun getWebhookWithToken(channel: Channel): GetWebhookWithTokenResponse { return client .get() + .uri(resolveTargetToken(channel)) .request() } - override suspend fun sendMessage(request: SendMessageRequest) { + override suspend fun sendMessage(channel: Channel, request: SendMessageRequest) { return client .post() + .uri(resolveTargetToken(channel)) .bodyValue(request) .request() } + + private fun resolveTargetToken(channel: Channel): String { + return targets[channel]?.token ?: throw NotFoundException(ErrorCode.NOT_FOUND_TARGET_TOKEN) + } } diff --git a/src/main/kotlin/com/hero/alignlab/client/discord/config/DiscordWebhookClientConfig.kt b/src/main/kotlin/com/hero/alignlab/client/discord/config/DiscordWebhookClientConfig.kt index bc3ee4a..74357b3 100644 --- a/src/main/kotlin/com/hero/alignlab/client/discord/config/DiscordWebhookClientConfig.kt +++ b/src/main/kotlin/com/hero/alignlab/client/discord/config/DiscordWebhookClientConfig.kt @@ -34,11 +34,21 @@ class DiscordWebhookClientConfig { val webclient = WebClientFactory.generate(discordWebhookConfig.url) - return SuspendableDiscordWebhookClient(webclient) + return SuspendableDiscordWebhookClient(webclient, discordWebhookConfig.channels) } data class Config( @field:NotBlank var url: String = "", - ) + var channels: Map = emptyMap() + ) { + data class Token( + @field:NotBlank + var token: String = "", + ) + + enum class Channel { + STATISTICS, DISCUSSION; + } + } } diff --git a/src/main/kotlin/com/hero/alignlab/domain/dev/application/DevDeleteService.kt b/src/main/kotlin/com/hero/alignlab/domain/dev/application/DevDeleteService.kt index 9cdde79..26a7bd2 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/dev/application/DevDeleteService.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/dev/application/DevDeleteService.kt @@ -85,4 +85,45 @@ class DevDeleteService( discussionRepository.deleteAllByUid(uid) discussionCommentRepository.deleteAllByUid(uid) } + + @Transactional + fun deleteAllWithoutUser(uid: Long) { + /* + /** 유저 정보 삭제 */ + credentialUserInfoRepository.deleteAllByUid(uid) + oauthUserInfoRepository.deleteAllByUid(uid) + userInfoRepository.deleteById(uid) + */ + + /** pose layout 데이터 삭제 */ + val poseLayoutIds = poseLayoutRepository.findAllByUid(uid).map { it.id } + + poseLayoutRepository.deleteAllByUid(uid) + poseLayoutPointRepository.deleteAllByPoseLayoutIdIn(poseLayoutIds) + + poseCountRepository.deleteAllByUid(uid) + val poseSnapshotIds = poseSnapshotRepository.findAllByUid(uid) + .map { it.id } + + poseSnapshotRepository.deleteAllByUid(uid) + poseKeyPointSnapshotRepository.deleteAllByPoseSnapshotIdIn(poseSnapshotIds) + + /** pose noti 제거 */ + poseNotificationRepository.deleteAllByUid(uid) + + /** syslog */ + systemActionLogRepository.deleteAllByUid(uid) + + /** img */ + imageMetadataRepository.deleteAllByUid(uid) + + /** group */ + groupRepository.deleteAllByOwnerUid(uid) + groupUserRepository.deleteAllByUid(uid) + groupUserScoreRepository.deleteAllByUid(uid) + + /** discussion */ + discussionRepository.deleteAllByUid(uid) + discussionCommentRepository.deleteAllByUid(uid) + } } diff --git a/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDeleteResource.kt b/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDeleteResource.kt index de8ebe1..1b0af70 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDeleteResource.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDeleteResource.kt @@ -23,6 +23,6 @@ class DevDeleteResource( dev: DevAuthUser, @PathVariable uid: Long, ) { - devDeleteService.deleteAll(uid) + devDeleteService.deleteAllWithoutUser(uid) } } diff --git a/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDiscordWebhookResource.kt b/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDiscordWebhookResource.kt index 99a6883..3fe6d75 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDiscordWebhookResource.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/dev/resource/DevDiscordWebhookResource.kt @@ -2,6 +2,7 @@ package com.hero.alignlab.domain.dev.resource import com.hero.alignlab.batch.statistics.job.HeroStatisticsJob import com.hero.alignlab.client.discord.client.DiscordWebhookClient +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig import com.hero.alignlab.client.discord.model.request.SendMessageRequest import com.hero.alignlab.config.swagger.SwaggerTag.DEV_TAG import com.hero.alignlab.domain.auth.model.DevAuthUser @@ -25,9 +26,10 @@ class DevDiscordWebhookResource( @PostMapping("/api/dev/v1/discord-webhooks/{id}") suspend fun sendMessage( dev: DevAuthUser, + @RequestParam channel: DiscordWebhookClientConfig.Config.Channel, @RequestParam message: String, ) { - discordWebhookClient.sendMessage(SendMessageRequest(message)) + discordWebhookClient.sendMessage(channel, SendMessageRequest(message)) } @Operation(summary = "discord webhook daily noti") diff --git a/src/main/kotlin/com/hero/alignlab/domain/discussion/application/DiscussionService.kt b/src/main/kotlin/com/hero/alignlab/domain/discussion/application/DiscussionService.kt index 5d3e963..56b96d0 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/discussion/application/DiscussionService.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/discussion/application/DiscussionService.kt @@ -7,12 +7,15 @@ import com.hero.alignlab.domain.discussion.domain.Discussion import com.hero.alignlab.domain.discussion.infrastructure.DiscussionRepository import com.hero.alignlab.domain.discussion.model.request.DiscussionRequest import com.hero.alignlab.domain.discussion.model.response.DiscussionResponse +import com.hero.alignlab.event.model.DiscussionEvent +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service @Service class DiscussionService( private val discussionRepository: DiscussionRepository, private val txTemplates: TransactionTemplates, + private val publisher: ApplicationEventPublisher, ) { suspend fun create(user: AuthUser, request: DiscussionRequest): DiscussionResponse { val discussion = Discussion( @@ -24,7 +27,11 @@ class DiscussionService( ) val createdDiscussion = txTemplates.writer.coExecute { - discussionRepository.save(discussion) + val discussion = discussionRepository.save(discussion) + + publisher.publishEvent(DiscussionEvent(discussion)) + + discussion } return DiscussionResponse.from(createdDiscussion) diff --git a/src/main/kotlin/com/hero/alignlab/event/listener/DiscussionEventListener.kt b/src/main/kotlin/com/hero/alignlab/event/listener/DiscussionEventListener.kt new file mode 100644 index 0000000..a553217 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/event/listener/DiscussionEventListener.kt @@ -0,0 +1,45 @@ +package com.hero.alignlab.event.listener + +import com.hero.alignlab.client.discord.client.DiscordWebhookClient +import com.hero.alignlab.client.discord.config.DiscordWebhookClientConfig +import com.hero.alignlab.client.discord.model.request.SendMessageRequest +import com.hero.alignlab.domain.user.application.UserInfoService +import com.hero.alignlab.event.model.DiscussionEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionalEventListener +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Component +class DiscussionEventListener( + private val discordWebhookClient: DiscordWebhookClient, + private val userInfoService: UserInfoService, +) { + @TransactionalEventListener + fun handle(event: DiscussionEvent) { + CoroutineScope(Dispatchers.IO + Job()).launch { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + + val userInfo = userInfoService.getUserInfo(event.discussion.uid) + + val message = """ + **문의하기 [${LocalDateTime.now().format(formatter)}]** + + - 유저명 : ${userInfo.nickname} + - 이메일 : ${event.discussion.email ?: "없음"} + - type : ${event.discussion.type} + - title : ${event.discussion.title} + - content : ${event.discussion.content} + """.trimIndent() + + discordWebhookClient.sendMessage( + channel = DiscordWebhookClientConfig.Config.Channel.DISCUSSION, + request = SendMessageRequest(message) + ) + } + } +} diff --git a/src/main/kotlin/com/hero/alignlab/event/listener/WithdrawEventListener.kt b/src/main/kotlin/com/hero/alignlab/event/listener/WithdrawEventListener.kt index bee7e65..74dad2d 100644 --- a/src/main/kotlin/com/hero/alignlab/event/listener/WithdrawEventListener.kt +++ b/src/main/kotlin/com/hero/alignlab/event/listener/WithdrawEventListener.kt @@ -1,9 +1,8 @@ package com.hero.alignlab.event.listener +import com.hero.alignlab.domain.dev.application.DevDeleteService import com.hero.alignlab.domain.group.application.GroupFacade import com.hero.alignlab.domain.group.infrastructure.GroupRepository -import com.hero.alignlab.domain.notification.infrastructure.PoseNotificationRepository -import com.hero.alignlab.domain.pose.infrastructure.* import com.hero.alignlab.event.model.WithdrawEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -16,14 +15,7 @@ class WithdrawEventListener( /** 탈퇴 회원이 그룹장인 경우 승계 작업 필요. */ private val groupFacade: GroupFacade, private val groupRepository: GroupRepository, - - /** 자세 데이터, 탈퇴회원이 있더라도 문제 없음. */ - private val poseCountRepository: PoseCountRepository, - private val poseSnapshotRepository: PoseSnapshotRepository, - private val poseKeyPointSnapshotRepository: PoseKeyPointSnapshotRepository, - private val poseLayoutRepository: PoseLayoutRepository, - private val poseLayoutPointRepository: PoseLayoutPointRepository, - private val poseNotificationRepository: PoseNotificationRepository, + private val devDeleteService: DevDeleteService, ) { /** * 현상황에서 그룹 승계 작업만 원활히 진행되면, 그외 데이터의 경우 탈퇴로 인한 문제는 발생하지 않는다. @@ -37,6 +29,8 @@ class WithdrawEventListener( if (group != null) { groupFacade.withdraw(group.id, event.uid) } + + devDeleteService.deleteAll(event.uid) } } } diff --git a/src/main/kotlin/com/hero/alignlab/event/model/Event.kt b/src/main/kotlin/com/hero/alignlab/event/model/Event.kt index 11e1e47..9685e64 100644 --- a/src/main/kotlin/com/hero/alignlab/event/model/Event.kt +++ b/src/main/kotlin/com/hero/alignlab/event/model/Event.kt @@ -3,6 +3,7 @@ package com.hero.alignlab.event.model import com.hero.alignlab.common.extension.mapper import com.hero.alignlab.common.extension.remoteIp import com.hero.alignlab.domain.auth.model.AUTH_TOKEN_KEY +import com.hero.alignlab.domain.discussion.domain.Discussion import com.hero.alignlab.domain.group.domain.Group import com.hero.alignlab.domain.pose.domain.PoseSnapshot import com.hero.alignlab.domain.pose.model.PoseSnapshotModel.KeyPoint @@ -62,3 +63,7 @@ data class SystemActionLogEvent( data class WithdrawEvent( val uid: Long, ) : BaseEvent() + +data class DiscussionEvent( + val discussion: Discussion, +) : BaseEvent() diff --git a/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt b/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt index 0b49cbc..212bf4a 100644 --- a/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt +++ b/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt @@ -59,5 +59,8 @@ enum class ErrorCode(val status: HttpStatus, val description: String) { /** Pose Layout Error Code */ NOT_FOUND_POSE_LAYOUT_ERROR(HttpStatus.NOT_FOUND, "포즈 레이아웃을 찾을 수 없습니다."), + + /** discord */ + NOT_FOUND_TARGET_TOKEN(HttpStatus.NOT_FOUND, "디스코드 발송 토큰이 없습니다."), ; } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 69af137..b0564cf 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -71,7 +71,12 @@ client: unlink-path: /v1/user/unlink discord: webhook: - url: + url: https://discord.com/api/webhooks + channels: + STATISTICS: + token: + DISCUSSION: + token: encrypt: key: