Skip to content

Commit

Permalink
Feat/#90. 유저가 신고 시 관리자에게 알림보내기 (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
JinDDung2 authored May 11, 2024
1 parent 33f3c9c commit 427812b
Show file tree
Hide file tree
Showing 17 changed files with 191 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class BoardService(
}

fun report(request: ReportBoardContentRequest, userId: Long): String {
val report = boardPostDomainService.report(request.toCreateReportRequest(), userId)
return "Successfully report. id=${report.id}. type:${report.postType}, reason:${report.reason}"
val event = boardPostDomainService.report(request.toCreateReportRequest(), userId)
return "Successfully report. id=${event.report.id}. type:${event.report.postType}, reason:${event.report.reason}"
}
}
1 change: 1 addition & 0 deletions adevspoon-domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
dependencies {
implementation(project(":adevspoon-common"))
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.slack.api:slack-api-client:1.38.0")
//querydsl
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class PostDomainErrorCode(
override val message: String,
): AdevspoonErrorCode {
MINIMUM_LIKE_COUNT(domain code 400 no 0, "좋아요 수는 음수일 수 없습니다."),
UNSUPPORTED_TYPE_FOR_CONTENT_ACCESS(domain code 400 no 1, "BOARD에서 지원하지 않는 타입입니다."),

BOARD_TAG_NOT_FOUND(domain code 404 no 0, "등록되지 않은 태그입니다."),
BOARD_POST_NOT_FOUND(domain code 404 no 1, "등록되지 않은 게시글입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ class DuplicateReportException(type: String, contentId: Long) : AdevspoonExcepti
class NegativeLikeCountExceptionForBoard(type: String, contentId: Long) : AdevspoonException(
MINIMUM_LIKE_COUNT,
detailReason = MINIMUM_LIKE_COUNT.message + " type: $type, id: $contentId"
)
)

class UnsupportedTypeForContentAccess() : AdevspoonException(UNSUPPORTED_TYPE_FOR_CONTENT_ACCESS)
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import com.adevspoon.domain.board.exception.*
import com.adevspoon.domain.board.repository.BoardCommentRepository
import com.adevspoon.domain.board.repository.BoardPostRepository
import com.adevspoon.domain.board.repository.BoardTagRepository
import com.adevspoon.domain.common.annotation.ActivityEvent
import com.adevspoon.domain.common.annotation.ActivityEventType
import com.adevspoon.domain.common.annotation.DomainService
import com.adevspoon.domain.common.annotation.*
import com.adevspoon.domain.common.entity.BaseEntity
import com.adevspoon.domain.common.entity.ReportEntity
import com.adevspoon.domain.common.entity.enums.ReportReason
import com.adevspoon.domain.common.event.ReportEvent
import com.adevspoon.domain.common.repository.ReportRepository
import com.adevspoon.domain.common.service.LikeDomainService
import com.adevspoon.domain.common.utils.CursorPageable
Expand Down Expand Up @@ -142,21 +141,20 @@ class BoardPostDomainService(
}
}

private fun getBoardPostEntity(postId: Long): BoardPostEntity =
boardPostRepository.findByIdOrNull(postId) ?: throw BoardPostNotFoundException(postId.toString())

@Transactional
fun report(request: CreateReportRequest, userId: Long): ReportEntity {
@AdminNotificationEvent(type = AdminMessageType.REPORT)
fun report(request: CreateReportRequest, userId: Long): ReportEvent {
val user = getUserEntity(userId)

val content = when (request.type) {
val entity = when (request.type) {
"BOARD_POST" -> getBoardPostEntity(request.contentId)
"BOARD_COMMENT" -> getBoardCommentEntity(request.contentId)
else -> throw IllegalArgumentException("Invalid content type")
}

checkOwnership(content, userId)
checkOwnershipAndGetContent(entity, userId)
checkReportExistence(request.type.toString(), request.contentId)
val content = getContent(entity)

val report = ReportEntity(
postType = request.type.toString().lowercase(Locale.getDefault()),
Expand All @@ -165,7 +163,8 @@ class BoardPostDomainService(
boardPostId = if (request.type == "BOARD_POST") request.contentId else null,
boardCommentId = if (request.type == "BOARD_COMMENT") request.contentId else null
)
return reportRepository.save(report)
reportRepository.save(report)
return ReportEvent(content = content, report = report)
}

private fun checkReportExistence(type: String, contentId: Long) {
Expand All @@ -174,8 +173,7 @@ class BoardPostDomainService(
}
}


private fun checkOwnership(contentEntity: BaseEntity, userId: Long) {
private fun checkOwnershipAndGetContent(contentEntity: BaseEntity, userId: Long){
if (contentEntity is BoardPostEntity) {
if (contentEntity.user.id == userId) {
throw SelfReportException()
Expand All @@ -189,6 +187,17 @@ class BoardPostDomainService(
}
}

private fun getContent(entity: BaseEntity): String {
return when (entity) {
is BoardPostEntity -> entity.content
is BoardCommentEntity -> entity.content
else -> throw UnsupportedTypeForContentAccess()
}
}

private fun getBoardPostEntity(postId: Long): BoardPostEntity =
boardPostRepository.findByIdOrNull(postId) ?: throw BoardPostNotFoundException(postId.toString())

private fun getBoardCommentEntity(commentId: Long): BoardCommentEntity =
boardCommentRepository.findByIdOrNull(commentId) ?: throw BoardCommentNotFoundException(commentId.toString())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.adevspoon.domain.common.annotation

enum class AdminMessageType {
REPORT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.adevspoon.domain.common.annotation

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class AdminNotificationEvent(
val type: AdminMessageType
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.adevspoon.domain.common.aop

import com.adevspoon.domain.common.annotation.AdminMessageType
import com.adevspoon.domain.common.annotation.AdminNotificationEvent
import com.adevspoon.domain.common.event.ReportEvent
import com.adevspoon.domain.common.exception.ReportEventInvalidReturnException
import org.aspectj.lang.JoinPoint
import org.aspectj.lang.annotation.AfterReturning
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.reflect.MethodSignature
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component


@Aspect
@Component
class AdminNotificationAdvisor (
private val eventPublisher: ApplicationEventPublisher
){

@AfterReturning(pointcut = "@annotation(com.adevspoon.domain.common.annotation.AdminNotificationEvent)", returning = "result")
fun afterReturningAdvice(joinPoint: JoinPoint, result: Any?) {
val notificationAnnotation = getAnnotation(joinPoint)
when (notificationAnnotation.type) {
AdminMessageType.REPORT -> bugReportEventPublish(result)
}
}

private fun getAnnotation(joinPoint: JoinPoint) =
(joinPoint.signature as MethodSignature).method
.getAnnotation(AdminNotificationEvent::class.java)

private fun bugReportEventPublish(result: Any?) {
(result as? ReportEvent)
?.let {
eventPublisher.publishEvent(it)
} ?: throw ReportEventInvalidReturnException()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ class ReportEntity(

@Column(name = "boardCommentId")
val boardCommentId: Long? = null
) : LegacyBaseEntity()
) : LegacyBaseEntity() {
fun getContentId(): Long = boardPostId ?: boardCommentId ?: post?.id ?: -1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.adevspoon.domain.common.event

import com.adevspoon.domain.common.entity.ReportEntity

data class ReportEvent(
val content: String,
val report: ReportEntity
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ enum class DomainCommonError(
FAIL_TO_LOCK_QUERY(DomainType.COMMON code 500 no 2, "서버 내부 오류입니다. 관리자에게 문의하세요"),
FAIL_TO_GET_LOCK(DomainType.COMMON code 500 no 3, "서버 내부 오류입니다. 관리자에게 문의하세요"),
FAIL_TO_RELEASE_LOCK(DomainType.COMMON code 500 no 4, "서버 내부 오류입니다. 관리자에게 문의하세요"),
LOCK_KEY_NOT_SET(DomainType.COMMON code 500 no 5, "서버 내부 오류입니다. 관리자에게 문의하세요");
LOCK_KEY_NOT_SET(DomainType.COMMON code 500 no 5, "서버 내부 오류입니다. 관리자에게 문의하세요"),
REPORT_EVENT_INVALID_RETURN(DomainType.COMMON code 500 no 5, "버그 레포트 이벤트 저장 중 오류가 발생했습니다.");
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ class DomainInvalidAttributeException(type: String? = null): AdevspoonException(
class DomainFailToLockQueryException: AdevspoonException(DomainCommonError.FAIL_TO_LOCK_QUERY)
class DomainFailToGetLockException(keyName: String? = null, reason: String? = null): AdevspoonException(DomainCommonError.FAIL_TO_GET_LOCK, internalLog = keyName?.let { "$it 획득 실패 (사유: $reason)" })
class DomainFailToReleaseLockException(keyName: String? = null, reason: String? = null): AdevspoonException(DomainCommonError.FAIL_TO_RELEASE_LOCK, internalLog = keyName?.let { "$it 해제 실패 (사유: $reason)" })
class DomainLockKeyNotSetException: AdevspoonException(DomainCommonError.LOCK_KEY_NOT_SET)
class DomainLockKeyNotSetException: AdevspoonException(DomainCommonError.LOCK_KEY_NOT_SET)

class ReportEventInvalidReturnException: AdevspoonException(DomainCommonError.REPORT_EVENT_INVALID_RETURN)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.adevspoon.domain.common.service

import com.adevspoon.domain.common.annotation.DomainService
import com.adevspoon.domain.common.event.ReportEvent
import org.springframework.scheduling.annotation.Async
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener

@DomainService
interface AdminNotificationService {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun sendReportNotification(event: ReportEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.adevspoon.domain.common.service

import com.adevspoon.domain.common.event.ReportEvent
import com.slack.api.Slack
import com.slack.api.model.Attachment
import com.slack.api.model.Field
import com.slack.api.webhook.WebhookPayloads
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import java.io.IOException
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class SlackNotificationService: AdminNotificationService{

@Value("\${slack.webhook.url}")
private lateinit var SLACK_WEBHOOK_URL: String
private val logger = LoggerFactory.getLogger(this.javaClass)

override fun sendReportNotification(event: ReportEvent) {
val slack = Slack.getInstance()
try {
slack.send(SLACK_WEBHOOK_URL, WebhookPayloads.payload { p ->
p.text("*New Bug Report* :warning:")
.attachments(listOf(generateSlackAttachment(event)))
})
} catch (e: IOException) {
logger.warn("IOException occurred when sending message to Slack", e)
}
}

private fun generateSlackAttachment(event: ReportEvent): Attachment {
val requestTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now())
val contentId = event.report.getContentId()
return Attachment.builder()
.color("fa8128")
.title("$requestTime 발생 내용")
.fields(listOf(
generateSlackField("Report reason", event.report.reason.toString()),
generateSlackField("Report Type", event.report.postType),
generateSlackField("Target", "id: $contentId\ncontent: ${event.content}"),
generateSlackField("Reported by", "id: ${event.report.user.id} nickname: ${event.report.user.nickname}")))
.build()
}

private fun generateSlackField(title: String, value: String): Field {
return Field.builder()
.title(title)
.value(value)
.valueShortEnough(false)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.adevspoon.domain.config

import com.adevspoon.domain.common.service.AdminNotificationService
import com.adevspoon.domain.common.service.SlackNotificationService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class AdminNotificationConfig {

@Bean
fun notificationService(): AdminNotificationService {
return SlackNotificationService()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ class AnswerRepositoryCustomImpl(
val firstDayOfMonth = LocalDate.of(year, month, 1)
val lastDayOfMonth = firstDayOfMonth.withDayOfMonth(firstDayOfMonth.lengthOfMonth())

val formattedDate = Expressions.stringTemplate("DATE_FORMAT({0}, {1})", answerEntity.createdAt, "%Y-%m-%d")
val results = jpaQueryFactory.select(
Expressions.stringTemplate("DATE_FORMAT({0}, {1})", answerEntity.createdAt, "%Y-%m-%d").`as`("date"),
formattedDate.`as`("date"),
answerEntity.id.count()
)
.from(answerEntity)
.where(answerEntity.createdAt.between(firstDayOfMonth.atStartOfDay(), lastDayOfMonth.atTime(23, 59, 59))
.and(answerEntity.user.eq(user)))
.groupBy(Expressions.stringTemplate("DATE_FORMAT({0}, {1})", answerEntity.createdAt, "%Y-%m-%d"))
.orderBy(Expressions.stringTemplate("DATE_FORMAT({0}, {1})", answerEntity.createdAt, "%Y-%m-%d").asc())
.groupBy(formattedDate)
.orderBy(formattedDate.asc())
.fetch()

return results.map { tuple ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.adevspoon.domain.techQuestion.service

import com.adevspoon.domain.common.annotation.ActivityEvent
import com.adevspoon.domain.common.annotation.ActivityEventType
import com.adevspoon.domain.common.annotation.DomainService
import com.adevspoon.domain.common.annotation.*
import com.adevspoon.domain.common.entity.ReportEntity
import com.adevspoon.domain.common.event.ReportEvent
import com.adevspoon.domain.common.repository.LikeRepository
import com.adevspoon.domain.common.repository.ReportRepository
import com.adevspoon.domain.common.service.LikeDomainService
Expand Down Expand Up @@ -125,14 +124,17 @@ class AnswerDomainService(
}

@Transactional
fun reportAnswer(answerId: Long, memberId: Long) {
@AdminNotificationEvent(type = AdminMessageType.REPORT)
fun reportAnswer(answerId: Long, memberId: Long) : ReportEvent{
val member = getMember(memberId)
val answer = getAnswerWithUserAndQuestion(answerId)
.takeIf { it.user.id != memberId }
?: throw QuestionAnswerReportNotAllowedException()
checkReport(member, answer)

reportRepository.save(ReportEntity(user = member, postType = "answer", post = answer))
val report = reportRepository.save(ReportEntity(user = member, postType = "answer", post = answer))

return ReportEvent(content = answer.answer ?: "",report = report)
}

private fun makeQuestionAnswerListInfo(
Expand Down

0 comments on commit 427812b

Please sign in to comment.