diff --git a/app/src/main/kotlin/com/wespot/config/EnableRetryConfig.kt b/app/src/main/kotlin/com/wespot/config/EnableRetryConfig.kt new file mode 100644 index 00000000..e6045789 --- /dev/null +++ b/app/src/main/kotlin/com/wespot/config/EnableRetryConfig.kt @@ -0,0 +1,10 @@ +package com.wespot.config + +import org.springframework.context.annotation.Configuration +import org.springframework.retry.annotation.EnableRetry + +@EnableRetry +@Configuration +class EnableRetryConfig { + +} diff --git a/app/src/main/kotlin/com/wespot/user/UserController.kt b/app/src/main/kotlin/com/wespot/user/UserController.kt index 4feee251..7297de95 100644 --- a/app/src/main/kotlin/com/wespot/user/UserController.kt +++ b/app/src/main/kotlin/com/wespot/user/UserController.kt @@ -10,8 +10,10 @@ import com.wespot.user.dto.response.UserSettingResponse import com.wespot.user.port.`in`.CheckedUserRestrictionUseCase import com.wespot.user.port.`in`.UserSettingUseCase import com.wespot.user.port.`in`.UserUseCase +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -33,6 +35,14 @@ class UserController( .body(response) } + @PostMapping("/allow/policy") + fun allowNewPolicy(policyType: PolicyType): ResponseEntity { + userUseCase.allowNewPolicy(policyType) + + return ResponseEntity.status(HttpStatus.CREATED) + .build() + } + @PutMapping("/me") fun updateProfile( @RequestBody profile: UpdateProfileRequest diff --git a/build.gradle.kts b/build.gradle.kts index 91f2e033..e4e49f40 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,10 @@ subprojects { dependencies { + // retry + implementation("org.springframework.retry:spring-retry") + implementation("org.springframework.boot:spring-boot-starter-aop") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") implementation("org.springdoc:springdoc-openapi-starter-webmvc-api:2.3.0") implementation("io.swagger.core.v3:swagger-core:2.2.19") diff --git a/common/src/main/kotlin/com/wespot/exception/GetLockFailException.kt b/common/src/main/kotlin/com/wespot/exception/GetLockFailException.kt new file mode 100644 index 00000000..2140f610 --- /dev/null +++ b/common/src/main/kotlin/com/wespot/exception/GetLockFailException.kt @@ -0,0 +1,5 @@ +package com.wespot.exception + +class GetLockFailException(message: String) : RuntimeException(message) { + +} diff --git a/common/src/main/kotlin/com/wespot/lock/ExecutorWithLock.kt b/common/src/main/kotlin/com/wespot/lock/ExecutorWithLock.kt new file mode 100644 index 00000000..8f135f76 --- /dev/null +++ b/common/src/main/kotlin/com/wespot/lock/ExecutorWithLock.kt @@ -0,0 +1,9 @@ +package com.wespot.lock + +interface ExecutorWithLock { + + fun execute(task: Runnable, lockKey: LockKey) + + fun execute(task: () -> RETURN, lockKey: LockKey): RETURN + +} diff --git a/common/src/main/kotlin/com/wespot/lock/LockKey.kt b/common/src/main/kotlin/com/wespot/lock/LockKey.kt new file mode 100644 index 00000000..ddc6a4b1 --- /dev/null +++ b/common/src/main/kotlin/com/wespot/lock/LockKey.kt @@ -0,0 +1,25 @@ +package com.wespot.lock + +import com.wespot.exception.CustomException +import org.springframework.http.HttpStatus + + +@JvmRecord +data class LockKey(val keywords: List) { + + init { + if (keywords.isEmpty()) { + throw CustomException( + message = "Lock Key는 최소 1개 이상의 Keyword를 가져야 합니다.", + status = HttpStatus.INTERNAL_SERVER_ERROR + ) + } + } + + constructor(vararg keywords: Any) : this(keywords.map { it.toString() }.toList()) + + val key: String + get() { + return keywords.joinToString(":") { it } + } +} diff --git a/core/src/main/kotlin/com/wespot/auth/service/SecurityUtils.kt b/core/src/main/kotlin/com/wespot/auth/service/SecurityUtils.kt index 8f6b6977..ea97e6a3 100644 --- a/core/src/main/kotlin/com/wespot/auth/service/SecurityUtils.kt +++ b/core/src/main/kotlin/com/wespot/auth/service/SecurityUtils.kt @@ -18,6 +18,7 @@ object SecurityUtils { fun getLoginUser(userPort: UserPort): User { val principal = SecurityContextHolder.getContext().authentication.principal as PrincipalDetails + return userPort.findByEmail(principal.username) ?: throw CustomException(HttpStatus.NOT_FOUND, ExceptionView.TOAST, "해당 계정이 존재하지 않습니다.") } diff --git a/core/src/main/kotlin/com/wespot/comment/service/PostCommentCreatedService.kt b/core/src/main/kotlin/com/wespot/comment/service/PostCommentCreatedService.kt index f70aad39..ac25eb52 100644 --- a/core/src/main/kotlin/com/wespot/comment/service/PostCommentCreatedService.kt +++ b/core/src/main/kotlin/com/wespot/comment/service/PostCommentCreatedService.kt @@ -9,6 +9,8 @@ import com.wespot.comment.port.`in`.PostCommentCreatedUseCase import com.wespot.comment.port.out.PostCommentPort import com.wespot.comment.port.out.PostValidatePort import com.wespot.exception.CustomException +import com.wespot.lock.ExecutorWithLock +import com.wespot.lock.LockKey import com.wespot.user.port.out.UserPort import org.springframework.http.HttpStatus import org.springframework.stereotype.Service @@ -19,25 +21,36 @@ class PostCommentCreatedService( private val userPort: UserPort, private val postCommentPort: PostCommentPort, private val postValidatePort: PostValidatePort, + private val executorWithLock: ExecutorWithLock, ) : PostCommentCreatedUseCase { @Transactional override fun createComment(postCommentCreatedRequest: PostCommentCreatedRequest): Long { - if (!postValidatePort.existsPostById(postCommentCreatedRequest.postId)) { - throw CustomException(status = HttpStatus.BAD_REQUEST, message = "해당하는 게시글을 찾을 수 없습니다.") - } - - val loginUser = SecurityUtils.getLoginUser(userPort = userPort) - val postComment = PostComment.of( - postId = postCommentCreatedRequest.postId, - content = postCommentCreatedRequest.content, - user = loginUser - ) + return executorWithLock.execute( + task = { + if (!postValidatePort.existsPostById(postCommentCreatedRequest.postId)) { + throw CustomException(status = HttpStatus.BAD_REQUEST, message = "해당하는 게시글을 찾을 수 없습니다.") + } + + val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } - val savedPostComment = postCommentPort.save(postComment) - EventUtils.publish(PostCommentCreatedEvent(savedPostComment)) + val postComment = PostComment.of( + postId = postCommentCreatedRequest.postId, + content = postCommentCreatedRequest.content, + user = loginUser + ) - return savedPostComment.id + val savedPostComment = postCommentPort.save(postComment) + EventUtils.publish(PostCommentCreatedEvent(savedPostComment)) + + savedPostComment.id + }, + lockKey = LockKey("postComment", "${postCommentCreatedRequest.postId}"), + ) } } diff --git a/core/src/main/kotlin/com/wespot/comment/service/PostCommentDeletedService.kt b/core/src/main/kotlin/com/wespot/comment/service/PostCommentDeletedService.kt index 29622b3e..7216e94f 100644 --- a/core/src/main/kotlin/com/wespot/comment/service/PostCommentDeletedService.kt +++ b/core/src/main/kotlin/com/wespot/comment/service/PostCommentDeletedService.kt @@ -5,7 +5,9 @@ import com.wespot.auth.service.SecurityUtils import com.wespot.comment.event.PostCommentDeleteEvent import com.wespot.comment.port.`in`.PostCommentDeletedUseCase import com.wespot.comment.port.out.PostCommentPort +import com.wespot.exception.CustomException import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -18,6 +20,11 @@ class PostCommentDeletedService( @Transactional override fun deleteComment(commentId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val postComment = postCommentPort.findById(id = commentId) ?: throw IllegalArgumentException("존재하지 않는 댓글입니다.") if (!postComment.isAuthor(loginUser.id)) { diff --git a/core/src/main/kotlin/com/wespot/comment/service/PostCommentLikeService.kt b/core/src/main/kotlin/com/wespot/comment/service/PostCommentLikeService.kt index ed60c80c..5402e998 100644 --- a/core/src/main/kotlin/com/wespot/comment/service/PostCommentLikeService.kt +++ b/core/src/main/kotlin/com/wespot/comment/service/PostCommentLikeService.kt @@ -7,6 +7,7 @@ import com.wespot.comment.port.out.PostCommentLikePort import com.wespot.comment.port.out.PostCommentPort import com.wespot.exception.CustomException import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -20,6 +21,11 @@ class PostCommentLikeService( @Transactional override fun likeComment(commentId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val postComment = postCommentPort.findById(commentId) ?: throw CustomException(message = "존재하지 않는 댓글입니다.") postCommentLikePort.findByPostCommentIdAndUserId(postCommentId = commentId, userId = loginUser.id) ?.let { diff --git a/core/src/main/kotlin/com/wespot/comment/service/PostCommentReportService.kt b/core/src/main/kotlin/com/wespot/comment/service/PostCommentReportService.kt index d2ea4003..1ca7d4ee 100644 --- a/core/src/main/kotlin/com/wespot/comment/service/PostCommentReportService.kt +++ b/core/src/main/kotlin/com/wespot/comment/service/PostCommentReportService.kt @@ -4,6 +4,7 @@ import com.wespot.auth.service.SecurityUtils import com.wespot.comment.PostCommentReport import com.wespot.comment.PostCommentReportReason import com.wespot.comment.dto.PostCommentReportRequest +import com.wespot.comment.event.PostCommentReportEvent import com.wespot.comment.port.`in`.PostCommentReportUseCase import com.wespot.comment.port.out.PostCommentPort import com.wespot.comment.port.out.PostCommentReportPort @@ -11,6 +12,8 @@ import com.wespot.exception.CustomException import com.wespot.report.ReportReasonWithCustomReason import com.wespot.report.port.out.ReportReasonPort import com.wespot.user.port.out.UserPort +import org.springframework.context.ApplicationEventPublisher +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -19,14 +22,24 @@ class PostCommentReportService( private val userPort: UserPort, private val postCommentReportPort: PostCommentReportPort, private val postCommentPort: PostCommentPort, - private val reportReasonPort: ReportReasonPort + private val reportReasonPort: ReportReasonPort, + private val applicationEventPublisher: ApplicationEventPublisher, ) : PostCommentReportUseCase { @Transactional override fun reportComment(commentId: Long, request: PostCommentReportRequest?) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val postComment = postCommentPort.findById(commentId) ?: throw CustomException(message = "존재하지 않는 댓글입니다.") + if (postComment.isAuthor(id = loginUser.id)) { + throw CustomException(message = "자신의 댓글은 신고할 수 없습니다.") + } + postCommentReportPort.findByPostCommentIdAndUserId(postCommentId = commentId, userId = loginUser.id) ?.let { postCommentReportPort.deleteById(it.id) @@ -56,6 +69,17 @@ class PostCommentReportService( val addedReportPostComment = postComment.addReport() postCommentPort.save(addedReportPostComment) + + val reportReason = postCommentReportReasons.map { it.reportReasonWithCustomReason.reason() } + .joinToString(separator = ", ") { it } + applicationEventPublisher.publishEvent( + PostCommentReportEvent( + postComment = postComment, + sender = loginUser, + receiver = postComment.user, + reason = reportReason, + ) + ) } } diff --git a/core/src/main/kotlin/com/wespot/common/dto/view/HotPostComponentResponse.kt b/core/src/main/kotlin/com/wespot/common/dto/view/HotPostComponentResponse.kt index 81bcc509..3deee559 100644 --- a/core/src/main/kotlin/com/wespot/common/dto/view/HotPostComponentResponse.kt +++ b/core/src/main/kotlin/com/wespot/common/dto/view/HotPostComponentResponse.kt @@ -21,6 +21,7 @@ class HotPostComponentResponse( } data class HotPostResponse( + val targetId: Long = 0L, val headerSection: HotPostHeaderSectionResponse, val infoSection: HotPostInfoSectionResponse, val createdAt: RichTextV2Response, @@ -63,6 +64,7 @@ class HotPostComponentResponse( ), posts = posts.map { post -> HotPostContentResponse.HotPostResponse( + targetId = post.targetId, headerSection = HotPostContentResponse.HotPostHeaderSectionResponse( profileImage = ImageContentV2Response.from(post.headerSection.profileImage), nickname = RichTextV2Response.from(post.headerSection.nickname) diff --git a/core/src/main/kotlin/com/wespot/message/service/listener/MessageBlockedEventListener.kt b/core/src/main/kotlin/com/wespot/message/service/listener/MessageBlockedEventListener.kt new file mode 100644 index 00000000..32b9dc5d --- /dev/null +++ b/core/src/main/kotlin/com/wespot/message/service/listener/MessageBlockedEventListener.kt @@ -0,0 +1,7 @@ +package com.wespot.message.service.listener + +import org.springframework.stereotype.Component + +@Component +class MessageBlockedEventListener { +} diff --git a/core/src/main/kotlin/com/wespot/message/service/listener/MessageEventListener.kt b/core/src/main/kotlin/com/wespot/message/service/listener/MessageEventListener.kt index 816713f1..deb8f4dd 100644 --- a/core/src/main/kotlin/com/wespot/message/service/listener/MessageEventListener.kt +++ b/core/src/main/kotlin/com/wespot/message/service/listener/MessageEventListener.kt @@ -1,8 +1,11 @@ package com.wespot.message.service.listener +import com.wespot.message.event.MessageV2BlockedEvent import com.wespot.message.port.`in`.CreateMessageUseCase import com.wespot.message.port.`in`.CreatedMessageV2UseCase +import com.wespot.user.block.BlockedUser import com.wespot.user.event.WelcomeMessageEvent +import com.wespot.user.port.out.BlockedUserPort import org.springframework.context.event.EventListener import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component @@ -14,7 +17,8 @@ import org.springframework.transaction.event.TransactionalEventListener @Component class MessageEventListener( private val createMessageUseCase: CreateMessageUseCase, - private val createdMessageV2UseCase: CreatedMessageV2UseCase + private val createdMessageV2UseCase: CreatedMessageV2UseCase, + private val blockedUserPort: BlockedUserPort, ) { @EventListener @@ -29,5 +33,38 @@ class MessageEventListener( createdMessageV2UseCase.welcomeMessage(event.signUpUser) } + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun handleMessageBlockedEvent(event: MessageV2BlockedEvent) { + val message = event.message + if (message.isAnonymousRoom()) { + return + } + + val sender = event.sender + + if (message.isBlockedBy(viewer = sender)) { + val blockedUser = BlockedUser.create( + blockerId = sender.id, + blockedId = event.receiver.id, + messageId = message.id, + isAlreadyBlocked = blockedUserPort.existsByBlockerIdAndBlockedIdAndMessageId( + blockerId = sender.id, + blockedId = event.receiver.id, + messageId = message.id + ) + ) + blockedUserPort.save(blockedUser) + return + } + + blockedUserPort.deleteByBlockerIdAndBlockedIdAndMessageId( + blockerId = sender.id, + blockedId = event.receiver.id, + messageId = message.id + ) + } + } diff --git a/core/src/main/kotlin/com/wespot/message/service/v2/AnswerMessageV2Service.kt b/core/src/main/kotlin/com/wespot/message/service/v2/AnswerMessageV2Service.kt index bc2b5fb6..f48def37 100644 --- a/core/src/main/kotlin/com/wespot/message/service/v2/AnswerMessageV2Service.kt +++ b/core/src/main/kotlin/com/wespot/message/service/v2/AnswerMessageV2Service.kt @@ -1,11 +1,13 @@ package com.wespot.message.service.v2 import com.wespot.auth.service.SecurityUtils +import com.wespot.exception.CustomException import com.wespot.message.dto.request.AnswerMessageRequest import com.wespot.message.port.`in`.AnswerMessageV2UseCase import com.wespot.message.port.out.MessageV2Port import com.wespot.message.v2.MessageRoom import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -19,6 +21,10 @@ class AnswerMessageV2Service( override fun answerMessage(messageRoomId: Long, answerMessageRequest: AnswerMessageRequest) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + if (loginUser.canNotUseMessage()) { + throw CustomException(message = "메시지 기능을 사용할 수 없는 사용자입니다.", status = HttpStatus.FORBIDDEN) + } + val message = messageV2Port.findById(id = messageRoomId) val messageDetails = messageV2Port.findAllByMessageRoomId(message.id) val alreadyUsedMessageOnToday = messageV2Port.countTodaySendMessages(senderId = loginUser.id) diff --git a/core/src/main/kotlin/com/wespot/message/service/v2/BlockedMessageV2Service.kt b/core/src/main/kotlin/com/wespot/message/service/v2/BlockedMessageV2Service.kt index 23f251d7..2d9acaf6 100644 --- a/core/src/main/kotlin/com/wespot/message/service/v2/BlockedMessageV2Service.kt +++ b/core/src/main/kotlin/com/wespot/message/service/v2/BlockedMessageV2Service.kt @@ -1,24 +1,40 @@ package com.wespot.message.service.v2 import com.wespot.auth.service.SecurityUtils +import com.wespot.exception.CustomException +import com.wespot.message.event.MessageV2BlockedEvent import com.wespot.message.port.`in`.BlockedMessageV2UseCase import com.wespot.message.port.out.MessageV2Port import com.wespot.user.port.out.UserPort +import org.springframework.context.ApplicationEventPublisher +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class BlockedMessageV2Service( private val userPort: UserPort, - private val messageV2Port: MessageV2Port + private val messageV2Port: MessageV2Port, + private val applicationEventPublisher: ApplicationEventPublisher, ) : BlockedMessageV2UseCase { @Transactional override fun blockMessage(messageId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseMessage()) { + throw CustomException(message = "메시지 기능을 사용할 수 없는 사용자입니다.", status = HttpStatus.FORBIDDEN) + } + val message = messageV2Port.findById(id = messageId) message.block(viewer = loginUser) messageV2Port.save(message) + val event = MessageV2BlockedEvent( + sender = loginUser, + receiver = message.receiverByViewer(viewer = loginUser), + message = message + ) + applicationEventPublisher.publishEvent(event) } } diff --git a/core/src/main/kotlin/com/wespot/message/service/v2/CreatedMessageV2Service.kt b/core/src/main/kotlin/com/wespot/message/service/v2/CreatedMessageV2Service.kt index cb6c53b7..1da403f5 100644 --- a/core/src/main/kotlin/com/wespot/message/service/v2/CreatedMessageV2Service.kt +++ b/core/src/main/kotlin/com/wespot/message/service/v2/CreatedMessageV2Service.kt @@ -27,6 +27,11 @@ class CreatedMessageV2Service( @Transactional override fun createMessage(createdMessageV2Request: CreatedMessageV2Request): MessageV2 { val sender = SecurityUtils.getLoginUser(userPort) + + if (sender.canNotUseMessage()) { + throw CustomException(message = "메시지 기능을 사용할 수 없는 사용자입니다.", status = HttpStatus.FORBIDDEN) + } + val receiver = userPort.findById(userId = createdMessageV2Request.receiverId) ?: throw CustomException( HttpStatus.NOT_FOUND, diff --git a/core/src/main/kotlin/com/wespot/message/service/v2/DeleteMessageV2Service.kt b/core/src/main/kotlin/com/wespot/message/service/v2/DeleteMessageV2Service.kt index 63e54fd6..fc9a3b63 100644 --- a/core/src/main/kotlin/com/wespot/message/service/v2/DeleteMessageV2Service.kt +++ b/core/src/main/kotlin/com/wespot/message/service/v2/DeleteMessageV2Service.kt @@ -1,11 +1,13 @@ package com.wespot.message.service.v2 import com.wespot.auth.service.SecurityUtils +import com.wespot.exception.CustomException import com.wespot.message.port.`in`.DeleteMessageV2UseCase import com.wespot.message.port.out.MessageV2Port import com.wespot.message.v2.MessageRoom import com.wespot.message.v2.MessageV2 import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -19,6 +21,10 @@ class DeleteMessageV2Service( override fun deleteMessage(messageId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + if (loginUser.canNotUseMessage()) { + throw CustomException(message = "메시지 기능을 사용할 수 없는 사용자입니다.", status = HttpStatus.FORBIDDEN) + } + val message = messageV2Port.findById(id = messageId) val allMessagesOfRoom = getAllMessagesOfRoom(message) val noMeaningNumber = 0 diff --git a/core/src/main/kotlin/com/wespot/message/service/v2/UpdatedMessageV2Service.kt b/core/src/main/kotlin/com/wespot/message/service/v2/UpdatedMessageV2Service.kt index b5658161..1c2d635d 100644 --- a/core/src/main/kotlin/com/wespot/message/service/v2/UpdatedMessageV2Service.kt +++ b/core/src/main/kotlin/com/wespot/message/service/v2/UpdatedMessageV2Service.kt @@ -20,6 +20,10 @@ class UpdatedMessageV2Service( override fun bookmarkMessage(messageId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + if (loginUser.canNotUseMessage()) { + throw CustomException(message = "메시지 기능을 사용할 수 없는 사용자입니다.", status = HttpStatus.FORBIDDEN) + } + val message = messageV2Port.findById(messageId) if (!message.isRoom()) { diff --git a/core/src/main/kotlin/com/wespot/notification/service/listener/CommentNotificationListener.kt b/core/src/main/kotlin/com/wespot/notification/service/listener/CommentNotificationListener.kt index 671e2937..b7a1f0fd 100644 --- a/core/src/main/kotlin/com/wespot/notification/service/listener/CommentNotificationListener.kt +++ b/core/src/main/kotlin/com/wespot/notification/service/listener/CommentNotificationListener.kt @@ -5,6 +5,7 @@ import com.wespot.common.NotificationUtil import com.wespot.exception.CustomException import com.wespot.notification.Notification import com.wespot.notification.NotificationType +import com.wespot.notification.port.out.NotificationPort import com.wespot.notification.service.NotificationHelper import com.wespot.post.port.out.PostNotificationPort import com.wespot.post.port.out.PostPort @@ -23,10 +24,11 @@ class CommentNotificationListener( private val postProfilePort: PostProfilePort, private val postNotificationPort: PostNotificationPort, private val notificationHelper: NotificationHelper, + private val notificationPort: NotificationPort, ) { @Async - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) @Transactional(propagation = Propagation.REQUIRES_NEW) fun listenCreatedPostCommentEvent(postCommentCreatedEvent: PostCommentCreatedEvent) { val postComment = postCommentCreatedEvent.postComment @@ -51,6 +53,8 @@ class CommentNotificationListener( ), ) } + .toList() + notificationPort.saveAll(notifications) val usersGroup = users.associateBy { it.id } notifications.forEach { notificationHelper.sendNotification(usersGroup[it.userId], it) } } diff --git a/core/src/main/kotlin/com/wespot/post/dto/response/PostCommentResponse.kt b/core/src/main/kotlin/com/wespot/post/dto/response/PostCommentResponse.kt index a9215bb6..b2ccccb5 100644 --- a/core/src/main/kotlin/com/wespot/post/dto/response/PostCommentResponse.kt +++ b/core/src/main/kotlin/com/wespot/post/dto/response/PostCommentResponse.kt @@ -23,16 +23,16 @@ data class PostCommentResponse( private const val VIEWER_NAME = "익명의 댓쓴이" fun of( - isPostOwner: Boolean = false, isCommentOwner: Boolean = false, + isPostCommentOwnerPostAuthor: Boolean = false, postComment: PostComment, - postProfile: PostProfile + postProfile: PostProfile, ): PostCommentResponse { return PostCommentResponse( id = postComment.id, authorImage = postProfile.url, isMe = isCommentOwner, - authorName = if (isPostOwner) OWNER_NAME else VIEWER_NAME, + authorName = if (isPostCommentOwnerPostAuthor) OWNER_NAME else VIEWER_NAME, content = postComment.content.content, likeCount = postComment.likeCount, hasPushedLike = postComment.postCommentStatusByViewer?.isViewerPushedLike ?: false, diff --git a/core/src/main/kotlin/com/wespot/post/dto/response/PostComponentResponse.kt b/core/src/main/kotlin/com/wespot/post/dto/response/PostComponentResponse.kt index 833266d6..7cbd9b49 100644 --- a/core/src/main/kotlin/com/wespot/post/dto/response/PostComponentResponse.kt +++ b/core/src/main/kotlin/com/wespot/post/dto/response/PostComponentResponse.kt @@ -139,6 +139,7 @@ data class PostComponentResponse( icon = IconV2Response.from(it.icon), text = RichTextV2Response.from(it.text), type = it.type, + isSelected = it.isSelected ) } } diff --git a/core/src/main/kotlin/com/wespot/post/port/out/PostBlockPort.kt b/core/src/main/kotlin/com/wespot/post/port/out/PostBlockPort.kt index fad2b564..7bdc1b51 100644 --- a/core/src/main/kotlin/com/wespot/post/port/out/PostBlockPort.kt +++ b/core/src/main/kotlin/com/wespot/post/port/out/PostBlockPort.kt @@ -12,4 +12,6 @@ interface PostBlockPort { fun deleteByPostId(postId: Long) + fun findAllByUserId(userId: Long): List + } diff --git a/core/src/main/kotlin/com/wespot/post/port/out/PostCategoryPort.kt b/core/src/main/kotlin/com/wespot/post/port/out/PostCategoryPort.kt index 739aa9f5..0b977318 100644 --- a/core/src/main/kotlin/com/wespot/post/port/out/PostCategoryPort.kt +++ b/core/src/main/kotlin/com/wespot/post/port/out/PostCategoryPort.kt @@ -10,4 +10,6 @@ interface PostCategoryPort { fun findAllByMajorCategoryName(majorCategoryName: String): List + fun findAllByName(name: String): List + } diff --git a/core/src/main/kotlin/com/wespot/post/port/out/PostPort.kt b/core/src/main/kotlin/com/wespot/post/port/out/PostPort.kt index bd328e68..82fea21a 100644 --- a/core/src/main/kotlin/com/wespot/post/port/out/PostPort.kt +++ b/core/src/main/kotlin/com/wespot/post/port/out/PostPort.kt @@ -10,36 +10,50 @@ interface PostPort { keyword: String, viewerId: Long? = null, inquirySize: Long, + blockPostIds: List, cursorId: Long? ): List - fun searchByTitle(title: String, viewerId: Long? = null): List + fun searchByTitle( + title: String, viewerId: Long? = null, blockPostIds: List, + ): List - fun searchByDescription(description: String, viewerId: Long? = null): List + fun searchByDescription( + description: String, viewerId: Long? = null, blockPostIds: List, + ): List - fun findById(postId: Long, viewerId: Long? = null): Post? + fun findById( + postId: Long, viewerId: Long? = null, + ): Post? - fun findAllByCategoryId(categoryId: Long, viewerId: Long? = null, inquirySize: Long, cursorId: Long?): List + fun findAllByCategoryId( + categoryId: Long, viewerId: Long? = null, inquirySize: Long, cursorId: Long?, blockPostIds: List, + ): List fun findAllByCategoryIdIn( categoryIds: List, viewerId: Long? = null, inquirySize: Long, + blockPostIds: List, cursorId: Long? ): List - fun findAllByUserId(authorId: Long, inquirySize: Long, cursorId: Long?): List + fun findAllByUserId( + authorId: Long, inquirySize: Long, cursorId: Long?, blockPostIds: List, + ): List fun findAllByPostIdIn( postIds: List, viewerId: Long? = null, inquirySize: Long, + blockPostIds: List, cursorId: Long? ): List fun findAllRecentPost( viewerId: Long? = null, inquirySize: Long, + blockPostIds: List, cursorId: Long? ): List diff --git a/core/src/main/kotlin/com/wespot/post/service/HotPostInquiryService.kt b/core/src/main/kotlin/com/wespot/post/service/HotPostInquiryService.kt index f82f0a89..3eb1400e 100644 --- a/core/src/main/kotlin/com/wespot/post/service/HotPostInquiryService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/HotPostInquiryService.kt @@ -2,6 +2,7 @@ package com.wespot.post.service import com.wespot.post.Post import com.wespot.post.port.`in`.HotPostInquiryUseCase +import com.wespot.post.port.out.PostBlockPort import com.wespot.post.port.out.PostPort import com.wespot.user.User import org.springframework.stereotype.Service @@ -10,13 +11,18 @@ import org.springframework.transaction.annotation.Transactional @Service class HotPostInquiryService( private val postPort: PostPort, + private val postBlockPort: PostBlockPort, ) : HotPostInquiryUseCase { @Transactional(readOnly = true) override fun topPost(user: User, countOfView: Int): List { val countOfPostToStatistic = 100L val posts = - postPort.findAllRecentPost(inquirySize = countOfPostToStatistic, viewerId = user.id, cursorId = null) + postPort.findAllRecentPost( + inquirySize = countOfPostToStatistic, + viewerId = user.id, + cursorId = null, + blockPostIds = postBlockPort.findAllByUserId(userId = user.id).map { it.postId }) return posts .sortedByDescending { it.createdAt } .sortedByDescending { it.scoreOfPost() } diff --git a/core/src/main/kotlin/com/wespot/post/service/ModifyPostNotificationSettingService.kt b/core/src/main/kotlin/com/wespot/post/service/ModifyPostNotificationSettingService.kt index 131a9ea0..b7d82497 100644 --- a/core/src/main/kotlin/com/wespot/post/service/ModifyPostNotificationSettingService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/ModifyPostNotificationSettingService.kt @@ -1,9 +1,11 @@ package com.wespot.post.service import com.wespot.auth.service.SecurityUtils +import com.wespot.exception.CustomException import com.wespot.post.dto.request.ModifyPostNotificationSettingRequest import com.wespot.post.port.`in`.ModifyPostNotificationSettingUseCase import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,6 +17,11 @@ class ModifyPostNotificationSettingService( @Transactional override fun changeSetting(modifyPostNotificationSettingRequest: ModifyPostNotificationSettingRequest) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + loginUser.changeSettings( isEnablePostNotification = modifyPostNotificationSettingRequest.isEnablePostNotification ) diff --git a/core/src/main/kotlin/com/wespot/post/service/PostBlockService.kt b/core/src/main/kotlin/com/wespot/post/service/PostBlockService.kt index 90649e2b..933a0a3a 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostBlockService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostBlockService.kt @@ -3,10 +3,13 @@ package com.wespot.post.service import com.wespot.auth.service.SecurityUtils import com.wespot.exception.CustomException import com.wespot.post.PostBlock +import com.wespot.post.event.PostBlockEvent import com.wespot.post.port.`in`.PostBlockUseCase import com.wespot.post.port.out.PostBlockPort import com.wespot.post.port.out.PostPort import com.wespot.user.port.out.UserPort +import org.springframework.context.ApplicationEventPublisher +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,18 +18,33 @@ class PostBlockService( private val userPort: UserPort, private val postBlockPort: PostBlockPort, private val postPort: PostPort, + private val applicationEventPublisher: ApplicationEventPublisher ) : PostBlockUseCase { @Transactional override fun blockPost(postId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val post = postPort.findById(postId = postId) ?: throw CustomException(message = "존재하지 않는 게시글입니다.") + if (post.isAuthor(loginUser.id)) { + throw CustomException(message = "자신의 게시글은 차단할 수 없습니다.") + } postBlockPort.findByPostIdAndUserId(postId = post.id, userId = loginUser.id) ?.let { postBlockPort.deleteById(it.id) } ?: run { postBlockPort.save(PostBlock(postId = post.id, userId = loginUser.id)) + val event = PostBlockEvent( + sender = loginUser, + receiver = post.user, + post = post + ) + applicationEventPublisher.publishEvent(event) } } diff --git a/core/src/main/kotlin/com/wespot/post/service/PostCategoryService.kt b/core/src/main/kotlin/com/wespot/post/service/PostCategoryService.kt index 43318ad7..5e06e6dd 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostCategoryService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostCategoryService.kt @@ -28,16 +28,19 @@ class PostCategoryService( override fun getCategories(): List { val ALL_INCLUDE_CATEGORY_NAME = "전체" val postCategories = PostCategories.from(postCategories = postCategoryPort.findAll()) - val eachMajorCategories = postCategories.eachMajorCategories var id = 1L val firstElement = FilterChip.of(id = id++, text = ALL_INCLUDE_CATEGORY_NAME) - val otherElements = eachMajorCategories.map { - FilterChip.of( - id = id++, - eachMajorCategory = it - ) - } + val otherElements = postCategories.eachMajorCategories + .map { it.postCategories } + .flatten() + .map { + FilterChip.of( + id = id++, + postCategory = it + ) + } + .toList() return (listOf(firstElement) + otherElements) .map { FilterChipResponse.from(it) } diff --git a/core/src/main/kotlin/com/wespot/post/service/PostCommentInquiryService.kt b/core/src/main/kotlin/com/wespot/post/service/PostCommentInquiryService.kt index 19c0731c..2d240503 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostCommentInquiryService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostCommentInquiryService.kt @@ -32,12 +32,14 @@ class PostCommentInquiryService( val userIdToProfile = postProfilePort.findByUserIdIn(userIds) .associateBy { it.userId } + + return postComments.map { PostCommentResponse.of( - isPostOwner = post.isAuthor(loginUser.id), isCommentOwner = it.isAuthor(loginUser.id), + isPostCommentOwnerPostAuthor = post.isAuthor(it.user.id), postComment = it, - postProfile = userIdToProfile[it.user.id]!! + postProfile = userIdToProfile[it.user.id]!!, ) } } diff --git a/core/src/main/kotlin/com/wespot/post/service/PostCreatedService.kt b/core/src/main/kotlin/com/wespot/post/service/PostCreatedService.kt index 89982ffe..1b6bed4a 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostCreatedService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostCreatedService.kt @@ -28,6 +28,11 @@ class PostCreatedService( @Transactional override fun createPost(createdPostRequest: CreatedPostRequest): Long { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val category = postCategoryPort.findById(createdPostRequest.categoryId) ?: throw CustomException( status = HttpStatus.BAD_REQUEST, diff --git a/core/src/main/kotlin/com/wespot/post/service/PostDeleteService.kt b/core/src/main/kotlin/com/wespot/post/service/PostDeleteService.kt index d8e91e21..d3a9ca18 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostDeleteService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostDeleteService.kt @@ -19,6 +19,11 @@ class PostDeleteService( @Transactional override fun deletePost(postId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val post = postPort.findById(postId = postId) ?: throw IllegalArgumentException("존재하지 않는 게시글입니다.") if (post.isAuthor(loginUser.id)) { diff --git a/core/src/main/kotlin/com/wespot/post/service/PostEditService.kt b/core/src/main/kotlin/com/wespot/post/service/PostEditService.kt index f45498a4..1eabea19 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostEditService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostEditService.kt @@ -9,6 +9,7 @@ import com.wespot.post.port.out.PostCategoryPort import com.wespot.post.port.out.PostPort import com.wespot.user.port.out.UserPort import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -24,6 +25,11 @@ class PostEditService( @Transactional override fun editPost(postId: Long, request: UpdatedPostRequest): Long { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val savedPost = postPort.findById(postId) ?: throw CustomException(message = "존재하지 않는 게시글입니다.") val category = postCategoryPort.findById(categoryId = request.categoryId) ?: throw CustomException(message = "존재하지 않는 카테고리입니다.") diff --git a/core/src/main/kotlin/com/wespot/post/service/PostInquiryService.kt b/core/src/main/kotlin/com/wespot/post/service/PostInquiryService.kt index 197dd9af..2099d6bb 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostInquiryService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostInquiryService.kt @@ -20,6 +20,7 @@ import com.wespot.post.nudge.server_driven.MessageComponent import com.wespot.post.nudge.server_driven.VoteComponent import com.wespot.post.port.`in`.PostInquiryUseCase import com.wespot.post.port.`in`.PostNudgeUseCase +import com.wespot.post.port.out.PostBlockPort import com.wespot.post.port.out.PostCategoryPort import com.wespot.post.port.out.PostPort import com.wespot.post.port.out.PostScrapPort @@ -29,6 +30,7 @@ import com.wespot.user.port.out.UserPort import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import kotlin.math.min @Service class PostInquiryService( @@ -37,6 +39,7 @@ class PostInquiryService( private val postPort: PostPort, private val postCommentPort: PostCommentPort, private val postScrapPort: PostScrapPort, + private val postBlockPort: PostBlockPort, private val nudgeModalUseCase: PostNudgeUseCase, ) : PostInquiryUseCase { @@ -57,7 +60,8 @@ class PostInquiryService( categoryId = postCategory.id, viewerId = loginUser.id, cursorId = cursorId, - inquirySize = inquirySize + 1 + inquirySize = inquirySize + 1, + blockPostIds = postBlockPort.findAllByUserId(userId = loginUser.id).map { it.postId } ) val data = posts.map { PostComponent.of(it, viewerId = loginUser.id, isCategoryScreen = true) } @@ -103,7 +107,7 @@ class PostInquiryService( ) val startSequence = countOfPostsViewed.toInt() + 1 - val endSequence = countOfPostsViewed.toInt() + inquirySize.toInt() + val endSequence = countOfPostsViewed.toInt() + min(inquirySize, posts.size.toLong()).toInt() val nudges = nudgeModalUseCase.findAllNudgeModalsBySequence( startSequence = startSequence, endSequence = endSequence, @@ -117,7 +121,6 @@ class PostInquiryService( data = data, lastCursorId = lastCursorId, hasNext = hasNext - ) } @@ -131,17 +134,19 @@ class PostInquiryService( return postPort.findAllRecentPost( viewerId = loginUser.id, inquirySize = inquirySize + 1, - cursorId = cursorId + cursorId = cursorId, + blockPostIds = postBlockPort.findAllByUserId(userId = loginUser.id).map { it.postId }, ) } - val categoryIds = postCategoryPort.findAllByMajorCategoryName(majorCategoryName) + val categoryIds = postCategoryPort.findAllByName(majorCategoryName) .map { it.id } return postPort.findAllByCategoryIdIn( categoryIds = categoryIds, viewerId = loginUser.id, inquirySize = inquirySize + 1, - cursorId = cursorId + cursorId = cursorId, + blockPostIds = postBlockPort.findAllByUserId(userId = loginUser.id).map { it.postId }, ) } @@ -209,12 +214,12 @@ class PostInquiryService( postId: Long, ): PostComponentResponse { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) - val post = postPort.findById(postId, viewerId = loginUser.id) ?: throw CustomException( + val post = postPort.findById(postId = postId, viewerId = loginUser.id) ?: throw CustomException( status = HttpStatus.BAD_REQUEST, message = "존재하지 않는 게시글입니다." ) - val postComponent = PostComponent.fromDetail(post, loginUser.id) + val postComponent = PostComponent.fromDetail(post = post, viewerId = loginUser.id) return PostComponentResponse.from(postComponent) } @@ -232,7 +237,8 @@ class PostInquiryService( postIds = postIds, viewerId = loginUser.id, inquirySize = inquirySize + 1, - cursorId = cursorId + cursorId = cursorId, + blockPostIds = postBlockPort.findAllByUserId(userId = loginUser.id).map { it.postId }, ) return postPagingResponse(posts, inquirySize, loginUser.id) @@ -252,7 +258,8 @@ class PostInquiryService( postIds = postIds, viewerId = loginUser.id, inquirySize = inquirySize + 1, - cursorId = cursorId + cursorId = cursorId, + blockPostIds = postBlockPort.findAllByUserId(userId = loginUser.id).map { it.postId }, ).let { posts -> return postPagingResponse(posts, inquirySize, loginUser.id) } @@ -268,7 +275,8 @@ class PostInquiryService( postPort.findAllByUserId( authorId = loginUser.id, inquirySize = inquirySize + 1, - cursorId = cursorId + cursorId = cursorId, + blockPostIds = postBlockPort.findAllByUserId(userId = loginUser.id).map { it.postId }, ).let { return postPagingResponse(it, inquirySize, loginUser.id) } } diff --git a/core/src/main/kotlin/com/wespot/post/service/PostLikeService.kt b/core/src/main/kotlin/com/wespot/post/service/PostLikeService.kt index 8228c8b9..29fe5fb2 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostLikeService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostLikeService.kt @@ -7,6 +7,7 @@ import com.wespot.post.port.`in`.PostLikeUseCase import com.wespot.post.port.out.PostLikePort import com.wespot.post.port.out.PostPort import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -20,6 +21,11 @@ class PostLikeService( @Transactional override fun likePost(postId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val post = postPort.findById(postId) ?: throw CustomException(message = "존재하지 않는 게시글입니다.") postLikePort.findByPostIdAndUserId(postId = postId, userId = loginUser.id) ?.let { diff --git a/core/src/main/kotlin/com/wespot/post/service/PostNotificationService.kt b/core/src/main/kotlin/com/wespot/post/service/PostNotificationService.kt index 622fd810..cf45a227 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostNotificationService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostNotificationService.kt @@ -7,6 +7,7 @@ import com.wespot.post.port.`in`.PostNotificationUseCase import com.wespot.post.port.out.PostNotificationPort import com.wespot.post.port.out.PostPort import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -20,6 +21,11 @@ class PostNotificationService( @Transactional override fun toggleNotification(postId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val post = postPort.findById(postId = postId) ?: throw CustomException(message = "존재하지 않는 게시글입니다.") postNotificationPort.findByPostIdAndUserId(postId = post.id, userId = loginUser.id) ?.let { postNotificationPort.deleteById(it.id) } diff --git a/core/src/main/kotlin/com/wespot/post/service/PostReportService.kt b/core/src/main/kotlin/com/wespot/post/service/PostReportService.kt index 81fae4e7..a98cadd8 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostReportService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostReportService.kt @@ -5,14 +5,18 @@ import com.wespot.exception.CustomException import com.wespot.post.PostReport import com.wespot.post.PostReportReason import com.wespot.post.dto.request.PostReportRequest +import com.wespot.post.event.PostReportEvent import com.wespot.post.port.`in`.PostReportUseCase import com.wespot.post.port.out.PostPort import com.wespot.post.port.out.PostReportPort import com.wespot.report.ReportReasonWithCustomReason import com.wespot.report.port.out.ReportReasonPort import com.wespot.user.port.out.UserPort +import org.springframework.context.ApplicationEventPublisher +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import kotlin.math.log @Service class PostReportService( @@ -20,17 +24,29 @@ class PostReportService( private val postPort: PostPort, private val postReportPort: PostReportPort, private val reportReasonPort: ReportReasonPort, + private val applicationEventPublisher: ApplicationEventPublisher ) : PostReportUseCase { @Transactional override fun reportPost(postId: Long, postReportRequest: PostReportRequest?) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val post = postPort.findById(postId) ?: throw CustomException(message = "존재하지 않는 게시글입니다.") + if (post.isAuthor(loginUser.id)) { + throw CustomException(message = "자신의 게시글은 신고할 수 없습니다.") + } + postReportPort.findByPostIdAndUserId(postId, loginUser.id) ?.let { + if (postReportRequest != null) { + throw CustomException(message = "이미 신고한 게시글입니다.") + } postReportPort.deleteById(id = it.id) } ?: run { @@ -56,6 +72,17 @@ class PostReportService( postReport.addReportReasons(postReportReasons) postReportPort.save(postReport) + + val reportReason = postReportReasons.map { it.reportReasonWithCustomReason.reason() } + .joinToString(separator = ", ") { it } + applicationEventPublisher.publishEvent( + PostReportEvent( + post = post, + sender = loginUser, + receiver = post.user, + reason = reportReason, + ) + ) } } diff --git a/core/src/main/kotlin/com/wespot/post/service/PostScrapService.kt b/core/src/main/kotlin/com/wespot/post/service/PostScrapService.kt index 56663306..55654cd6 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostScrapService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostScrapService.kt @@ -7,6 +7,7 @@ import com.wespot.post.port.`in`.PostScrapUseCase import com.wespot.post.port.out.PostPort import com.wespot.post.port.out.PostScrapPort import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -20,6 +21,11 @@ class PostScrapService( @Transactional override fun scrapPost(postId: Long) { val loginUser = SecurityUtils.getLoginUser(userPort = userPort) + + if (loginUser.canNotUseCommunity()) { + throw CustomException(status = HttpStatus.FORBIDDEN, message = "커뮤니티 이용이 제한된 사용자입니다.") + } + val post = postPort.findById(postId) ?: throw CustomException(message = "존재하지 않는 게시글입니다.") postScrapPort.findByPostIdAndUserId(postId = post.id, userId = loginUser.id) ?.let { diff --git a/core/src/main/kotlin/com/wespot/post/service/PostSearchedService.kt b/core/src/main/kotlin/com/wespot/post/service/PostSearchedService.kt index 9f2e2862..8cf2ae9a 100644 --- a/core/src/main/kotlin/com/wespot/post/service/PostSearchedService.kt +++ b/core/src/main/kotlin/com/wespot/post/service/PostSearchedService.kt @@ -4,6 +4,7 @@ import com.wespot.auth.service.SecurityUtils import com.wespot.common.dto.PostPagingResponse import com.wespot.post.dto.response.PostComponentResponse import com.wespot.post.port.`in`.PostSearchedUseCase +import com.wespot.post.port.out.PostBlockPort import com.wespot.post.port.out.PostPort import com.wespot.post.server_driven.PostComponent import com.wespot.post.vo.PostSearchKeyword @@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional class PostSearchedService( private val postPort: PostPort, private val userPort: UserPort, + private val postBlockPort: PostBlockPort, ) : PostSearchedUseCase { @Transactional(readOnly = true) @@ -30,7 +32,8 @@ class PostSearchedService( keyword = keywordsToSearch, viewerId = loginUser.id, inquirySize = inquirySize + 1, - cursorId = cursorId + cursorId = cursorId, + blockPostIds = postBlockPort.findAllByUserId(loginUser.id).map { it.postId } ) val data = posts.map { PostComponent.of(it, loginUser.id) }.map { PostComponentResponse.from(it) } diff --git a/core/src/main/kotlin/com/wespot/report/dto/ReportReasonResponse.kt b/core/src/main/kotlin/com/wespot/report/dto/ReportReasonResponse.kt index 881df3b1..9242ca0c 100644 --- a/core/src/main/kotlin/com/wespot/report/dto/ReportReasonResponse.kt +++ b/core/src/main/kotlin/com/wespot/report/dto/ReportReasonResponse.kt @@ -4,14 +4,16 @@ import com.wespot.report.ReportReason data class ReportReasonResponse( val id: Long, - val reason: String + val reason: String, + val isReasonEditable: Boolean, ) { companion object { fun from(reportReason: ReportReason): ReportReasonResponse { return ReportReasonResponse( id = reportReason.id, - reason = reportReason.content + reason = reportReason.content, + isReasonEditable = reportReason.canReceiveReason, ) } } diff --git a/core/src/main/kotlin/com/wespot/report/dto/ReportResponse.kt b/core/src/main/kotlin/com/wespot/report/dto/ReportResponse.kt index 87216e3d..9c2ec2fd 100644 --- a/core/src/main/kotlin/com/wespot/report/dto/ReportResponse.kt +++ b/core/src/main/kotlin/com/wespot/report/dto/ReportResponse.kt @@ -1,5 +1,5 @@ package com.wespot.report.dto data class ReportResponse( - val id: Long + val id: Long, ) diff --git a/core/src/main/kotlin/com/wespot/report/port/out/ReportPort.kt b/core/src/main/kotlin/com/wespot/report/port/out/ReportPort.kt index be8a3a5a..18391aae 100644 --- a/core/src/main/kotlin/com/wespot/report/port/out/ReportPort.kt +++ b/core/src/main/kotlin/com/wespot/report/port/out/ReportPort.kt @@ -9,4 +9,6 @@ interface ReportPort { fun save(report: Report): Report + fun deleteByReportTypeAndTargetIdAndReceiverId(reportType: ReportType, targetId: Long, receiverId: Long) + } diff --git a/core/src/main/kotlin/com/wespot/report/service/CreatedReportService.kt b/core/src/main/kotlin/com/wespot/report/service/CreatedReportService.kt new file mode 100644 index 00000000..0bcafdf6 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/report/service/CreatedReportService.kt @@ -0,0 +1,53 @@ +package com.wespot.report.service + +import com.wespot.exception.CustomException +import com.wespot.report.Report +import com.wespot.report.ReportType +import com.wespot.report.port.out.ReportPort +import com.wespot.user.port.out.RestrictionPort +import com.wespot.user.port.out.UserPort +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CreatedReportService( + val reportPort: ReportPort, + val userPort: UserPort, + val restrictionPort: RestrictionPort, +) { + + @Transactional + fun saveReport( + reportType: ReportType, + targetId: Long, + senderId: Long, + receiverId: Long, + content: String = "", + ) { + val report = Report( + reportType = reportType, + targetId = targetId, + senderId = senderId, + receiverId = receiverId, + content = content + ) + changeRestriction(report, receiverId, reportType) + } + + fun changeRestriction(newReport: Report? = null, receiverId: Long, reportType: ReportType) { + val reports = reportPort.findAllByReceiverIdAndReportType(receiverId, reportType) + + val targetUser = userPort.findById(receiverId) ?: throw CustomException(message = "존재하지 않는 유저입니다.") + val restriction = targetUser.restriction + val newRestriction = restriction.receivedNewReport( + restrictionCategory = reportType.toRestrictionCategory(), + reportCount = reports.size.toLong() + if (newReport != null) 1 else 0 + ) + targetUser.restrict(newRestriction) + + newReport?.let { reportPort.save(newReport) } + restrictionPort.save(newRestriction) + userPort.save(targetUser) + } + +} diff --git a/core/src/main/kotlin/com/wespot/report/service/SavedReportService.kt b/core/src/main/kotlin/com/wespot/report/service/SavedReportService.kt index fba8cfa0..f4e9b44e 100644 --- a/core/src/main/kotlin/com/wespot/report/service/SavedReportService.kt +++ b/core/src/main/kotlin/com/wespot/report/service/SavedReportService.kt @@ -1,6 +1,5 @@ package com.wespot.report.service -import com.google.common.io.ByteArrayDataInput import com.wespot.auth.service.SecurityUtils import com.wespot.exception.CustomException import com.wespot.exception.ExceptionView @@ -42,7 +41,7 @@ class SavedReportService( val reports = findAllUserReportByReportType(targetUser, reportRequest.reportType) val savedReport = executeReport(report = report, receiver = targetUser, reports = reports) - return ReportResponse(savedReport.id) + return ReportResponse(id = savedReport.id) } private fun findLoginUser(): User { diff --git a/core/src/main/kotlin/com/wespot/report/service/listener/CreatedReportEventListener.kt b/core/src/main/kotlin/com/wespot/report/service/listener/CreatedReportEventListener.kt new file mode 100644 index 00000000..637f63e8 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/report/service/listener/CreatedReportEventListener.kt @@ -0,0 +1,101 @@ +package com.wespot.report.service.listener + +import com.wespot.comment.event.PostCommentBlockEvent +import com.wespot.comment.event.PostCommentReportEvent +import com.wespot.message.event.MessageV2BlockedEvent +import com.wespot.post.event.PostBlockEvent +import com.wespot.post.event.PostReportEvent +import com.wespot.report.ReportType +import com.wespot.report.port.out.ReportPort +import com.wespot.report.service.CreatedReportService +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class CreatedReportEventListener( + val createdReportService: CreatedReportService, + val reportPort: ReportPort, +) { + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun handleCreatedMessageBlockEvent(event: MessageV2BlockedEvent) { + val message = event.message + + if (message.isBlockedBy(viewer = event.sender)) { + createdReportService.saveReport( + reportType = ReportType.MESSAGE, + targetId = event.message.id, + senderId = event.sender.id, + receiverId = event.receiver.id, + content = "메시지 차단" + ) + return + } + + reportPort.deleteByReportTypeAndTargetIdAndReceiverId( + reportType = ReportType.MESSAGE, + targetId = event.message.id, + receiverId = event.receiver.id + ) + createdReportService.changeRestriction(receiverId = event.receiver.id, reportType = ReportType.MESSAGE) + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun handleCreatedPostReportEvent(event: PostReportEvent) { + createdReportService.saveReport( + reportType = ReportType.COMMUNITY, + targetId = event.post.id, + senderId = event.sender.id, + receiverId = event.receiver.id, + content = event.reason + ) + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun handleCreatedPostBlockEvent(event: PostBlockEvent) { + createdReportService.saveReport( + reportType = ReportType.COMMUNITY, + targetId = event.post.id, + senderId = event.sender.id, + receiverId = event.receiver.id, + content = "게시글 차단" + ) + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun handleCreatedPostCommentReportEvent(event: PostCommentReportEvent) { + createdReportService.saveReport( + reportType = ReportType.COMMUNITY, + targetId = event.postComment.id, + senderId = event.sender.id, + receiverId = event.receiver.id, + content = event.reason + ) + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun handleCreatedPostCommentBlockEvent(event: PostCommentBlockEvent) { + createdReportService.saveReport( + reportType = ReportType.COMMUNITY, + targetId = event.postComment.id, + senderId = event.sender.id, + receiverId = event.receiver.id, + content = "댓글 차단" + ) + } + +} diff --git a/core/src/main/kotlin/com/wespot/user/dto/request/ModifiedSettingRequest.kt b/core/src/main/kotlin/com/wespot/user/dto/request/ModifiedSettingRequest.kt index 393d26ae..0cbf9edd 100644 --- a/core/src/main/kotlin/com/wespot/user/dto/request/ModifiedSettingRequest.kt +++ b/core/src/main/kotlin/com/wespot/user/dto/request/ModifiedSettingRequest.kt @@ -3,5 +3,6 @@ package com.wespot.user.dto.request data class ModifiedSettingRequest( val isEnableVoteNotification: Boolean?, val isEnableMessageNotification: Boolean?, - val isEnableMarketingNotification: Boolean? + val isEnableMarketingNotification: Boolean?, + val isEnablePostNotification: Boolean?, ) diff --git a/core/src/main/kotlin/com/wespot/user/dto/response/UserResponse.kt b/core/src/main/kotlin/com/wespot/user/dto/response/UserResponse.kt index 5d8626e7..fcde81c3 100644 --- a/core/src/main/kotlin/com/wespot/user/dto/response/UserResponse.kt +++ b/core/src/main/kotlin/com/wespot/user/dto/response/UserResponse.kt @@ -10,7 +10,8 @@ data class UserResponse( val schoolName : String, val grade : Int, val classNumber : Int, - val profile: ProfileResponse? + val profile: ProfileResponse?, + val needToAnnounceAboutPolicy: Boolean = false, ) { companion object { @@ -24,7 +25,8 @@ data class UserResponse( schoolName = school, grade = user.grade, classNumber = user.classNumber, - profile = user.profile.let { ProfileResponse.from(it) } + profile = user.profile.let { ProfileResponse.from(it) }, + needToAnnounceAboutPolicy = user.needToAnnounceAboutPolicy(), ) } diff --git a/core/src/main/kotlin/com/wespot/user/dto/response/UserSettingResponse.kt b/core/src/main/kotlin/com/wespot/user/dto/response/UserSettingResponse.kt index d2d6a8ac..79c4123b 100644 --- a/core/src/main/kotlin/com/wespot/user/dto/response/UserSettingResponse.kt +++ b/core/src/main/kotlin/com/wespot/user/dto/response/UserSettingResponse.kt @@ -6,13 +6,15 @@ data class UserSettingResponse( val isEnableVoteNotification: Boolean, val isEnableMessageNotification: Boolean, val isEnableMarketingNotification: Boolean, + val isEnablePostNotification: Boolean = true ) { companion object { fun from(user: User) = UserSettingResponse( isEnableVoteNotification = user.isEnableVoteNotification(), isEnableMessageNotification = user.isEnableMessageNotification(), - isEnableMarketingNotification = user.isEnableMarketingNotification() + isEnableMarketingNotification = user.isEnableMarketingNotification(), + isEnablePostNotification = user.isEnablePostNotification() ) } } diff --git a/core/src/main/kotlin/com/wespot/user/port/in/UserUseCase.kt b/core/src/main/kotlin/com/wespot/user/port/in/UserUseCase.kt index 966b053e..a690404f 100644 --- a/core/src/main/kotlin/com/wespot/user/port/in/UserUseCase.kt +++ b/core/src/main/kotlin/com/wespot/user/port/in/UserUseCase.kt @@ -1,5 +1,6 @@ package com.wespot.user.port.`in` +import com.wespot.user.PolicyType import com.wespot.user.dto.request.SearchUserRequest import com.wespot.user.dto.request.UpdateProfileRequest import com.wespot.user.dto.response.BackgroundListResponse @@ -18,4 +19,6 @@ interface UserUseCase { fun characters(): CharacterListResponse + fun allowNewPolicy(policyType: PolicyType): Long + } diff --git a/core/src/main/kotlin/com/wespot/user/port/out/UserPolicyAgreementPort.kt b/core/src/main/kotlin/com/wespot/user/port/out/UserPolicyAgreementPort.kt new file mode 100644 index 00000000..eed677c9 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/user/port/out/UserPolicyAgreementPort.kt @@ -0,0 +1,9 @@ +package com.wespot.user.port.out + +import com.wespot.user.UserPolicyAgreement + +interface UserPolicyAgreementPort { + + fun save(userPolicyAgreement: UserPolicyAgreement): UserPolicyAgreement + +} diff --git a/core/src/main/kotlin/com/wespot/user/port/out/UserPort.kt b/core/src/main/kotlin/com/wespot/user/port/out/UserPort.kt index 42fe09a6..c81b7718 100644 --- a/core/src/main/kotlin/com/wespot/user/port/out/UserPort.kt +++ b/core/src/main/kotlin/com/wespot/user/port/out/UserPort.kt @@ -35,6 +35,7 @@ interface UserPort { cursorId: Long?, pageable: Pageable, loginUserId: Long, + blockedUserIds: List, ): List fun findAll(): List @@ -46,6 +47,7 @@ interface UserPort { cursorSchoolTypeOrder: Int?, cursorId: Long?, loginUserId: Long, + blockedUserIds: List, ): Long fun countBySchoolIdAndGradeAndClassNumber( diff --git a/core/src/main/kotlin/com/wespot/user/service/ModifyMessageV2SettingService.kt b/core/src/main/kotlin/com/wespot/user/service/ModifyMessageV2SettingService.kt index 87046820..70e20306 100644 --- a/core/src/main/kotlin/com/wespot/user/service/ModifyMessageV2SettingService.kt +++ b/core/src/main/kotlin/com/wespot/user/service/ModifyMessageV2SettingService.kt @@ -1,9 +1,11 @@ package com.wespot.user.service import com.wespot.auth.service.SecurityUtils +import com.wespot.exception.CustomException import com.wespot.user.dto.request.MessageV2Setting import com.wespot.user.port.`in`.ModifyMessageV2SettingUseCase import com.wespot.user.port.out.UserPort +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,6 +17,11 @@ class ModifyMessageV2SettingService( @Transactional override fun changeSetting(toChangeSetting: MessageV2Setting) { val loginUser = SecurityUtils.getLoginUser(userPort) + + if (loginUser.canNotUseMessage()) { + throw CustomException(message = "메시지 기능을 사용할 수 없는 사용자입니다.", status = HttpStatus.FORBIDDEN) + } + loginUser.changeSettings( isEnableMessage = toChangeSetting.isEnableMessage, ) diff --git a/core/src/main/kotlin/com/wespot/user/service/SearchUserService.kt b/core/src/main/kotlin/com/wespot/user/service/SearchUserService.kt index c6a0f0ae..5af1a238 100644 --- a/core/src/main/kotlin/com/wespot/user/service/SearchUserService.kt +++ b/core/src/main/kotlin/com/wespot/user/service/SearchUserService.kt @@ -11,6 +11,7 @@ import com.wespot.user.dto.CursorSearchData import com.wespot.user.dto.response.UserListResponse import com.wespot.user.dto.response.UserResponse import com.wespot.user.port.`in`.SearchUserUseCase +import com.wespot.user.port.out.BlockedUserPort import com.wespot.user.port.out.UserPort import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable @@ -21,7 +22,8 @@ import org.springframework.stereotype.Service @Service class SearchUserService( private val userPort: UserPort, - private val schoolPort: SchoolPort + private val schoolPort: SchoolPort, + private val blockedUserPort: BlockedUserPort ) : SearchUserUseCase { override fun searchUsers( @@ -31,9 +33,16 @@ class SearchUserService( val loginUser = SecurityUtils.getLoginUser(userPort) validateLoginUserRegulation(loginUser) val pageable = PageRequest.of(0, 10, Sort.by("id")) + val blockedUserIds = blockedUserPort.findAllByBlockerId(blockerId = loginUser.id) + .map { it.blockedId } if (cursorId == 0L) { - return fetchFirstPage(keyword = keyword, pageable = pageable, loginUserId = loginUser.id) + return fetchFirstPage( + keyword = keyword, + pageable = pageable, + loginUserId = loginUser.id, + blockedUserIds = blockedUserIds + ) } val cursorData = fetchCursorData(cursorId) @@ -44,7 +53,8 @@ class SearchUserService( cursorSchoolTypeOrder = cursorData.cursorSchoolTypeOrder, cursorId = cursorId, pageable = pageable, - loginUserId = loginUser.id + loginUserId = loginUser.id, + blockedUserIds = blockedUserIds ) val totalCount = userPort.countUsersAfterCursor( @@ -53,7 +63,8 @@ class SearchUserService( cursorSchoolName = cursorData.cursorSchoolName, cursorSchoolTypeOrder = cursorData.cursorSchoolTypeOrder, cursorId = cursorId, - loginUserId = loginUser.id + loginUserId = loginUser.id, + blockedUserIds = blockedUserIds ) return buildUserListResponse(users = users, pageable = pageable, totalCount = totalCount) @@ -69,6 +80,7 @@ class SearchUserService( keyword: String, pageable: Pageable, loginUserId: Long, + blockedUserIds: List, ): UserListResponse { val users = userPort.searchUsers( name = keyword, @@ -77,7 +89,8 @@ class SearchUserService( cursorSchoolTypeOrder = null, cursorId = null, pageable = pageable, - loginUserId = loginUserId + loginUserId = loginUserId, + blockedUserIds = blockedUserIds, ) val totalCount = userPort.countUsersAfterCursor( @@ -86,7 +99,8 @@ class SearchUserService( cursorSchoolName = null, cursorSchoolTypeOrder = null, cursorId = null, - loginUserId = loginUserId + loginUserId = loginUserId, + blockedUserIds = blockedUserIds, ) return buildUserListResponse(users = users, pageable = pageable, totalCount = totalCount) diff --git a/core/src/main/kotlin/com/wespot/user/service/UserService.kt b/core/src/main/kotlin/com/wespot/user/service/UserService.kt index 962b4276..42e6897a 100644 --- a/core/src/main/kotlin/com/wespot/user/service/UserService.kt +++ b/core/src/main/kotlin/com/wespot/user/service/UserService.kt @@ -1,20 +1,14 @@ package com.wespot.user.service import com.wespot.auth.service.SecurityUtils.getLoginUser -import com.wespot.exception.CustomException -import com.wespot.exception.ExceptionView -import com.wespot.school.School import com.wespot.school.port.out.SchoolPort -import com.wespot.user.User +import com.wespot.user.PolicyType import com.wespot.user.UserIntroduction +import com.wespot.user.UserPolicyAgreement import com.wespot.user.dto.request.UpdateProfileRequest import com.wespot.user.dto.response.* import com.wespot.user.port.`in`.UserUseCase -import com.wespot.user.port.out.ProfileBackgroundPort -import com.wespot.user.port.out.ProfileIconPort -import com.wespot.user.port.out.ProfilePort -import com.wespot.user.port.out.UserPort -import org.springframework.http.HttpStatus +import com.wespot.user.port.out.* import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -25,7 +19,8 @@ class UserService( private val schoolPort: SchoolPort, private val profilePort: ProfilePort, private val profileBackgroundPort: ProfileBackgroundPort, - private val profileIconPort: ProfileIconPort + private val profileIconPort: ProfileIconPort, + private val userPolicyAgreementPort: UserPolicyAgreementPort, ) : UserUseCase { override fun me(): UserResponse { @@ -71,4 +66,12 @@ class UserService( return CharacterListResponse.from(characterResponses) } + @Transactional + override fun allowNewPolicy(policyType: PolicyType): Long { + val loginUser = getLoginUser(userPort = userPort) + val userPolicyAgreement = UserPolicyAgreement(userId = loginUser.id, policyType = policyType) + + return userPolicyAgreementPort.save(userPolicyAgreement).id + } + } diff --git a/core/src/main/kotlin/com/wespot/user/service/UserSettingService.kt b/core/src/main/kotlin/com/wespot/user/service/UserSettingService.kt index ddfacd19..d2cc99bf 100644 --- a/core/src/main/kotlin/com/wespot/user/service/UserSettingService.kt +++ b/core/src/main/kotlin/com/wespot/user/service/UserSettingService.kt @@ -19,7 +19,8 @@ class UserSettingService( loginUser.changeSettings( isEnableVoteNotification = modifiedSettingRequest.isEnableVoteNotification, isEnableMessageNotification = modifiedSettingRequest.isEnableMessageNotification, - isEnableMarketingNotification = modifiedSettingRequest.isEnableMarketingNotification + isEnableMarketingNotification = modifiedSettingRequest.isEnableMarketingNotification, + isEnablePostNotification = modifiedSettingRequest.isEnablePostNotification ) userPort.save(loginUser) } diff --git a/domain/src/main/kotlin/com/wespot/comment/event/PostCommentBlockEvent.kt b/domain/src/main/kotlin/com/wespot/comment/event/PostCommentBlockEvent.kt new file mode 100644 index 00000000..7ce0147e --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/comment/event/PostCommentBlockEvent.kt @@ -0,0 +1,11 @@ +package com.wespot.comment.event + +import com.wespot.comment.PostComment +import com.wespot.user.User + +data class PostCommentBlockEvent( + val sender: User, + val receiver: User, + val postComment: PostComment +) { +} diff --git a/domain/src/main/kotlin/com/wespot/comment/event/PostCommentReportEvent.kt b/domain/src/main/kotlin/com/wespot/comment/event/PostCommentReportEvent.kt new file mode 100644 index 00000000..2405a0c6 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/comment/event/PostCommentReportEvent.kt @@ -0,0 +1,12 @@ +package com.wespot.comment.event + +import com.wespot.comment.PostComment +import com.wespot.user.User + +data class PostCommentReportEvent( + val sender: User, + val receiver: User, + val reason: String, + val postComment: PostComment +) { +} diff --git a/domain/src/main/kotlin/com/wespot/message/event/MessageV2BlockedEvent.kt b/domain/src/main/kotlin/com/wespot/message/event/MessageV2BlockedEvent.kt new file mode 100644 index 00000000..5424d459 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/message/event/MessageV2BlockedEvent.kt @@ -0,0 +1,11 @@ +package com.wespot.message.event + +import com.wespot.message.v2.MessageV2 +import com.wespot.user.User + +data class MessageV2BlockedEvent( + val sender: User, + val receiver: User, + val message: MessageV2 +) { +} diff --git a/domain/src/main/kotlin/com/wespot/message/v2/MessageDetail.kt b/domain/src/main/kotlin/com/wespot/message/v2/MessageDetail.kt index 1e0710fb..e918cc29 100644 --- a/domain/src/main/kotlin/com/wespot/message/v2/MessageDetail.kt +++ b/domain/src/main/kotlin/com/wespot/message/v2/MessageDetail.kt @@ -21,14 +21,16 @@ data class MessageDetail( viewer: User, message: MessageV2, alreadyUsedMessageOnToday: Int, - isLatestMessage: Boolean + isLatestMessage: Boolean, + roomMessage: MessageV2, ): MessageDetail { return MessageDetail( isReceived = message.isReceived(viewer = viewer), isSend = message.isSent(viewer = viewer), isAbleToAnswer = if (isLatestMessage) message.isAbleToAnswer( viewer = viewer, - alreadyUsedMessageOnToday = alreadyUsedMessageOnToday + alreadyUsedMessageOnToday = alreadyUsedMessageOnToday, + roomMessage = roomMessage ) else false, isRead = message.isRead(viewer = viewer), message = message @@ -45,7 +47,7 @@ data class MessageDetail( return message.createdAt } - fun createAnswerMessage(sender: User, alreadyUsedMessageOnToday: Int, content: MessageContent): MessageV2 { + fun createAnswerMessage(sender: User, alreadyUsedMessageOnToday: Int, content: MessageContent, roomMessage: MessageV2): MessageV2 { if (!isAbleToAnswer) { throw CustomException( message = "답장할 수 있는 쪽지가 아닙니다.", @@ -65,7 +67,8 @@ data class MessageDetail( return message.answerMessage( viewer = sender, alreadyUsedMessageOnToday = alreadyUsedMessageOnToday, - content = content + content = content, + roomMessage = roomMessage ) } diff --git a/domain/src/main/kotlin/com/wespot/message/v2/MessageDetails.kt b/domain/src/main/kotlin/com/wespot/message/v2/MessageDetails.kt index c9f1525c..97adb3ce 100644 --- a/domain/src/main/kotlin/com/wespot/message/v2/MessageDetails.kt +++ b/domain/src/main/kotlin/com/wespot/message/v2/MessageDetails.kt @@ -20,7 +20,8 @@ data class MessageDetails( viewer = viewer, message = message, alreadyUsedMessageOnToday = alreadyUsedMessageOnToday, - isLatestMessage = index == messages.lastIndex + isLatestMessage = index == messages.lastIndex, + roomMessage = messages.first { it.isRoom() } ) } .distinct() @@ -52,7 +53,8 @@ data class MessageDetails( return toAnswerMessage.createAnswerMessage( sender = sender, alreadyUsedMessageOnToday = alreadyUsedMessageOnToday, - content = content + content = content, + roomMessage = messages.first().message ) } diff --git a/domain/src/main/kotlin/com/wespot/message/v2/MessageRoom.kt b/domain/src/main/kotlin/com/wespot/message/v2/MessageRoom.kt index 45712dfe..fb41b98b 100644 --- a/domain/src/main/kotlin/com/wespot/message/v2/MessageRoom.kt +++ b/domain/src/main/kotlin/com/wespot/message/v2/MessageRoom.kt @@ -120,7 +120,7 @@ data class MessageRoom( } fun isBlockedByMe(): Boolean { - return roomMessage.isBlockedByMe(viewer = viewer) + return roomMessage.isBlockedBy(viewer = viewer) } fun isReceiverEver(): Boolean { diff --git a/domain/src/main/kotlin/com/wespot/message/v2/MessageV2.kt b/domain/src/main/kotlin/com/wespot/message/v2/MessageV2.kt index 307a7e55..822fa415 100644 --- a/domain/src/main/kotlin/com/wespot/message/v2/MessageV2.kt +++ b/domain/src/main/kotlin/com/wespot/message/v2/MessageV2.kt @@ -48,7 +48,7 @@ data class MessageV2( companion object { -// const val COUNT_OF_MAX_ABLE_TO_SEND_MESSAGE_PER_DAY = 3 + // const val COUNT_OF_MAX_ABLE_TO_SEND_MESSAGE_PER_DAY = 3 const val COUNT_OF_MAX_ABLE_TO_SEND_MESSAGE_PER_DAY = 100 // 개발하는 동안 100개로 유지 fun createInitial( @@ -272,7 +272,7 @@ data class MessageV2( return isReceiverBookmarked } - fun isBlockedByMe(viewer: User): Boolean { + fun isBlockedBy(viewer: User): Boolean { if (viewer.isMeSender(senderId = sender.id)) { return isSenderBlocked } @@ -302,10 +302,11 @@ data class MessageV2( return sender.profile.iconUrl } - fun isAbleToAnswer(viewer: User, alreadyUsedMessageOnToday: Int): Boolean { + fun isAbleToAnswer(viewer: User, alreadyUsedMessageOnToday: Int, roomMessage: MessageV2): Boolean { return alreadyUsedMessageOnToday < COUNT_OF_MAX_ABLE_TO_SEND_MESSAGE_PER_DAY && viewer.isMeReceiver(receiverId = receiver.id) && isAbleToUseMessage(sender, receiver) + && roomMessage.isNotBlockedByAnybody() } fun isSameUserProfileAndNotAnonymous(viewer: User): Boolean { @@ -333,7 +334,20 @@ data class MessageV2( return viewer.isMeSender(senderId = sender.id) || viewer.isMeReceiver(receiverId = receiver.id) } - fun answerMessage(viewer: User, alreadyUsedMessageOnToday: Int, content: MessageContent): MessageV2 { + fun answerMessage( + viewer: User, + alreadyUsedMessageOnToday: Int, + content: MessageContent, + roomMessage: MessageV2 + ): MessageV2 { + if (!roomMessage.isRoom()) { + throw CustomException( + message = "room message가 쪽지방이 아닙니다.", + status = HttpStatus.INTERNAL_SERVER_ERROR, + view = ExceptionView.TOAST, + ) + } + if (!isAbleToUseMessage(sender, receiver)) { throw CustomException( message = "쪽지 기능이 비활성화 되어 있는 유저가 존재합니다.", @@ -358,7 +372,13 @@ data class MessageV2( ) } - // TODO : 상대방에게 답장할 수 있는지 확인 (차단 여부) + if (roomMessage.isBlockedByAnybody()) { + throw CustomException( + message = "쪽지방에서 차단된 상태에서는 답장할 수 없습니다.", + status = HttpStatus.BAD_REQUEST, + view = ExceptionView.TOAST, + ) + } val newMessage = MessageV2( id = 0L, @@ -406,6 +426,14 @@ data class MessageV2( return newMessage } + private fun isBlockedByAnybody(): Boolean { + return isSenderBlocked || isReceiverBlocked + } + + private fun isNotBlockedByAnybody(): Boolean { + return !isBlockedByAnybody() + } + private fun isUserOwner(user: User): Boolean { return user.id == messageRoomOwnerId } @@ -587,4 +615,16 @@ data class MessageV2( return 31 * createdAt.hashCode() + content.hashCode() } + fun receiverByViewer(viewer: User): User { + if (viewer.isMeSender(senderId = sender.id)) { + return receiver + } + + return sender + } + + fun isAnonymousRoom(): Boolean { + return isRoom() && anonymousProfile != null + } + } diff --git a/domain/src/main/kotlin/com/wespot/post/PostReport.kt b/domain/src/main/kotlin/com/wespot/post/PostReport.kt index 1b598a07..ea1d344e 100644 --- a/domain/src/main/kotlin/com/wespot/post/PostReport.kt +++ b/domain/src/main/kotlin/com/wespot/post/PostReport.kt @@ -1,5 +1,6 @@ package com.wespot.post +import com.wespot.exception.CustomException import java.time.LocalDateTime class PostReport( @@ -11,6 +12,10 @@ class PostReport( ) { fun addReportReasons(postReportReasons: List) { + require(postReportReasons.isNotEmpty()) { + throw CustomException(message = "신고 사유는 최소 1개 이상이어야 합니다.") + } + postReportReasons.forEach { this.postReportReasons.plus(it) } diff --git a/domain/src/main/kotlin/com/wespot/post/event/PostBlockEvent.kt b/domain/src/main/kotlin/com/wespot/post/event/PostBlockEvent.kt new file mode 100644 index 00000000..3dbba794 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/post/event/PostBlockEvent.kt @@ -0,0 +1,11 @@ +package com.wespot.post.event + +import com.wespot.post.Post +import com.wespot.user.User + +data class PostBlockEvent( + val sender: User, + val receiver: User, + val post: Post +) { +} diff --git a/domain/src/main/kotlin/com/wespot/post/event/PostReportEvent.kt b/domain/src/main/kotlin/com/wespot/post/event/PostReportEvent.kt new file mode 100644 index 00000000..ddeef3bb --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/post/event/PostReportEvent.kt @@ -0,0 +1,12 @@ +package com.wespot.post.event + +import com.wespot.post.Post +import com.wespot.user.User + +data class PostReportEvent( + val sender: User, + val receiver: User, + val reason: String, + val post: Post +) { +} diff --git a/domain/src/main/kotlin/com/wespot/post/nudge/server_driven/HotPostComponent.kt b/domain/src/main/kotlin/com/wespot/post/nudge/server_driven/HotPostComponent.kt index fb420a1a..664bfa61 100644 --- a/domain/src/main/kotlin/com/wespot/post/nudge/server_driven/HotPostComponent.kt +++ b/domain/src/main/kotlin/com/wespot/post/nudge/server_driven/HotPostComponent.kt @@ -29,6 +29,7 @@ class HotPostComponent( } data class HotPost( + val targetId: Long = 0L, val headerSection: HotPostHeaderSection, val infoSection: HotPostInfoSection, val createdAt: RichTextV2, @@ -39,6 +40,7 @@ class HotPostComponent( fun from(post: Post): HotPost { return HotPost( + targetId = post.id, headerSection = HotPostHeaderSection( profileImage = ImageContentV2( url = post.profile.url, diff --git a/domain/src/main/kotlin/com/wespot/report/Report.kt b/domain/src/main/kotlin/com/wespot/report/Report.kt index cbfb287f..3be7fe0d 100644 --- a/domain/src/main/kotlin/com/wespot/report/Report.kt +++ b/domain/src/main/kotlin/com/wespot/report/Report.kt @@ -7,13 +7,13 @@ import org.springframework.http.HttpStatus import java.time.LocalDateTime data class Report( - val id: Long, + val id: Long = 0L, val reportType: ReportType, - val targetId: Long, + val targetId: Long = 0L, val senderId: Long, val receiverId: Long, - val content: String, - val createdAt: LocalDateTime + val content: String = "", + val createdAt: LocalDateTime = LocalDateTime.now(), ) { companion object { diff --git a/domain/src/main/kotlin/com/wespot/report/ReportReasonWithCustomReason.kt b/domain/src/main/kotlin/com/wespot/report/ReportReasonWithCustomReason.kt index 9aae81c9..711421eb 100644 --- a/domain/src/main/kotlin/com/wespot/report/ReportReasonWithCustomReason.kt +++ b/domain/src/main/kotlin/com/wespot/report/ReportReasonWithCustomReason.kt @@ -11,4 +11,12 @@ data class ReportReasonWithCustomReason( } } + fun reason(): String { + if (reportReason.canReceiveReason) { + return customReason ?: "" + } + + return reportReason.content + } + } diff --git a/domain/src/main/kotlin/com/wespot/report/ReportType.kt b/domain/src/main/kotlin/com/wespot/report/ReportType.kt index 1765637e..34e92474 100644 --- a/domain/src/main/kotlin/com/wespot/report/ReportType.kt +++ b/domain/src/main/kotlin/com/wespot/report/ReportType.kt @@ -1,5 +1,20 @@ package com.wespot.report +import com.wespot.user.restriction.RestrictionCategory + enum class ReportType(val value: String) { - MESSAGE("쪽지"), VOTE("투표"); + + MESSAGE("쪽지"), + VOTE("투표"), + COMMUNITY("커뮤니티"), + ; + + fun toRestrictionCategory(): RestrictionCategory { + return when (this) { + MESSAGE -> RestrictionCategory.MESSAGE + VOTE -> RestrictionCategory.VOTE + COMMUNITY -> RestrictionCategory.COMMUNITY + } + } + } diff --git a/domain/src/main/kotlin/com/wespot/user/PolicyType.kt b/domain/src/main/kotlin/com/wespot/user/PolicyType.kt new file mode 100644 index 00000000..d37078f7 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/user/PolicyType.kt @@ -0,0 +1,8 @@ +package com.wespot.user + +enum class PolicyType { + + NEW_POLICY_ABOUT_ACCOUNT, + ; + +} diff --git a/domain/src/main/kotlin/com/wespot/user/RestrictionType.kt b/domain/src/main/kotlin/com/wespot/user/RestrictionType.kt index 43c77133..3294eaf0 100644 --- a/domain/src/main/kotlin/com/wespot/user/RestrictionType.kt +++ b/domain/src/main/kotlin/com/wespot/user/RestrictionType.kt @@ -2,11 +2,16 @@ package com.wespot.user enum class RestrictionType { NONE, // 제한 x + PERMANENT_BAN_VOTE_REPORT, // 투표 제보로 인한 영구제한 + TEMPORARY_BAN_MESSAGE_REPORT, // 메시지 신고로 인한 제한 - PERMANENT_BAN_MESSAGE_REPORT; // 메시지 신고로 인한 영구제한 + PERMANENT_BAN_MESSAGE_REPORT, // 메시지 신고로 인한 영구제한 + + TEMPORARY_BAN_COMMUNITY_REPORT,// 커뮤니티 관련 신고 및 차단으로 인한 제한 + PERMANENT_BAN_COMMUNITY_REPORT // 커뮤니티 관련 신고 및 차단으로 인한 영구제한 + ; - // restriction type이 vote 인지, message 인지 확인하는 메서드를 작성해줘 fun isVoteRestriction(): Boolean { return this == PERMANENT_BAN_VOTE_REPORT } @@ -15,4 +20,8 @@ enum class RestrictionType { return this == TEMPORARY_BAN_MESSAGE_REPORT || this == PERMANENT_BAN_MESSAGE_REPORT } + fun isCommunityRestriction(): Boolean { + return this == TEMPORARY_BAN_COMMUNITY_REPORT || this == PERMANENT_BAN_COMMUNITY_REPORT + } + } diff --git a/domain/src/main/kotlin/com/wespot/user/User.kt b/domain/src/main/kotlin/com/wespot/user/User.kt index 90f0a4cf..40de467b 100644 --- a/domain/src/main/kotlin/com/wespot/user/User.kt +++ b/domain/src/main/kotlin/com/wespot/user/User.kt @@ -25,6 +25,7 @@ data class User( val social: Social, val userConsent: UserConsent, var restriction: Restriction, + val userPolicyAgreements: List, val createdAt: LocalDateTime, val updatedAt: LocalDateTime, val withdrawalStatus: WithdrawalStatus, @@ -33,7 +34,6 @@ data class User( val withdrawalCompleteAt: LocalDateTime?, ) { - fun updateProfile( introduction: String, ) = @@ -54,6 +54,8 @@ data class User( social = social, userConsent = userConsent, restriction = restriction, + userPolicyAgreements = userPolicyAgreements, + createdAt = createdAt, updatedAt = LocalDateTime.now(), withdrawalStatus = withdrawalStatus, @@ -79,6 +81,8 @@ data class User( setting = setting, social = social, userConsent = userConsent, + userPolicyAgreements = userPolicyAgreements, + createdAt = createdAt, restriction = restriction, updatedAt = LocalDateTime.now(), @@ -106,6 +110,8 @@ data class User( setting = setting, social = social, userConsent = userConsent, + userPolicyAgreements = userPolicyAgreements, + createdAt = createdAt, restriction = restriction, updatedAt = LocalDateTime.now(), @@ -147,6 +153,7 @@ data class User( withdrawalRequestAt = withdrawalRequestAt, withdrawalCancelAt = withdrawalCancelAt, withdrawalCompleteAt = LocalDateTime.now(), + userPolicyAgreements = userPolicyAgreements, ) companion object { @@ -191,6 +198,7 @@ data class User( withdrawalRequestAt = null, withdrawalCancelAt = null, withdrawalCompleteAt = null, + userPolicyAgreements = mutableListOf(), ) fun update( @@ -222,7 +230,8 @@ data class User( withdrawalStatus = user.withdrawalStatus, withdrawalRequestAt = user.withdrawalRequestAt, withdrawalCancelAt = user.withdrawalCancelAt, - withdrawalCompleteAt = user.withdrawalCompleteAt + withdrawalCompleteAt = user.withdrawalCompleteAt, + userPolicyAgreements = user.userPolicyAgreements, ) } @@ -239,6 +248,30 @@ data class User( this.restriction = restriction } + fun canNotUseVote(): Boolean { + if (isWithDraw()) { + return true + } + + return restriction.canNotUseVoteFeature() + } + + fun canNotUseMessage(): Boolean { + if (isWithDraw()) { + return true + } + + return restriction.canNotUseMessageFeature() + } + + fun canNotUseCommunity(): Boolean { + if (isWithDraw()) { + return true + } + + return restriction.canNotUseCommunityFeature() + } + fun changeSettings( isEnableVoteNotification: Boolean? = null, isEnableMessageNotification: Boolean? = null, @@ -333,4 +366,10 @@ data class User( return setting.isEnablePostNotification } + fun needToAnnounceAboutPolicy(): Boolean { + val standardDate = LocalDate.of(2025, 10, 15) + + return createdAt.isBefore(standardDate.atStartOfDay()) && userPolicyAgreements.none { it.policyType == PolicyType.NEW_POLICY_ABOUT_ACCOUNT } + } + } diff --git a/domain/src/main/kotlin/com/wespot/user/UserPolicyAgreement.kt b/domain/src/main/kotlin/com/wespot/user/UserPolicyAgreement.kt new file mode 100644 index 00000000..135266c3 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/user/UserPolicyAgreement.kt @@ -0,0 +1,8 @@ +package com.wespot.user + +data class UserPolicyAgreement( + val id: Long = 0L, + val userId: Long, + val policyType: PolicyType, +) { +} diff --git a/domain/src/main/kotlin/com/wespot/user/block/BlockedUser.kt b/domain/src/main/kotlin/com/wespot/user/block/BlockedUser.kt index 04ee3939..41109f91 100644 --- a/domain/src/main/kotlin/com/wespot/user/block/BlockedUser.kt +++ b/domain/src/main/kotlin/com/wespot/user/block/BlockedUser.kt @@ -3,14 +3,15 @@ package com.wespot.user.block import com.wespot.exception.CustomException import com.wespot.exception.ExceptionView import org.springframework.http.HttpStatus +import java.time.LocalDate import java.time.LocalDateTime data class BlockedUser( - val id: Long, + val id: Long = 0L, val blockerId: Long, val blockedId: Long, val messageId: Long, - val createdAt: LocalDateTime + val createdAt: LocalDateTime = LocalDateTime.now() ) { companion object { diff --git a/domain/src/main/kotlin/com/wespot/user/restriction/CommunityRestriction.kt b/domain/src/main/kotlin/com/wespot/user/restriction/CommunityRestriction.kt new file mode 100644 index 00000000..78a4fab4 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/user/restriction/CommunityRestriction.kt @@ -0,0 +1,57 @@ +package com.wespot.user.restriction + +import com.wespot.user.RestrictionType +import java.time.LocalDate + +data class CommunityRestriction( + val restrictionType: RestrictionType = RestrictionType.NONE, + val releaseDate: LocalDate = PERMANENT_BAN_DATE +) { + + companion object { + + private val PERMANENT_BAN_DATE = LocalDate.of(9999, 12, 31) + private const val PERMANENT_BAN_DAY = Long.MAX_VALUE + + private val RESTRICTION_RULE: List = listOf( + RestrictionRule(20, PERMANENT_BAN_DAY), + RestrictionRule(15, 15), + RestrictionRule(10, 7), + RestrictionRule(5, 3), + RestrictionRule(0, 0), + ) + + } + + fun getCurrentRestrictionBasedOnTime(date: LocalDate): CommunityRestriction { + if (releaseDate.isBefore(date)) { + return CommunityRestriction() + } + + return this + } + + fun isKeepRestriction(): Boolean { + return restrictionType != RestrictionType.NONE + } + + fun receivedNewReport(reportCount: Long): CommunityRestriction { + val restrictionDay = RESTRICTION_RULE + .firstOrNull { reportCount >= it.standard } + ?.restrictionDay ?: 0L + + if (restrictionDay == 0L) { + return this + } + + if (restrictionDay == PERMANENT_BAN_DAY) { + return CommunityRestriction(RestrictionType.PERMANENT_BAN_COMMUNITY_REPORT, PERMANENT_BAN_DATE) + } + + return CommunityRestriction( + RestrictionType.TEMPORARY_BAN_COMMUNITY_REPORT, + LocalDate.now().plusDays(restrictionDay) + ) + } + +} diff --git a/domain/src/main/kotlin/com/wespot/user/restriction/MessageRestriction.kt b/domain/src/main/kotlin/com/wespot/user/restriction/MessageRestriction.kt index 29844f4c..e02c017b 100644 --- a/domain/src/main/kotlin/com/wespot/user/restriction/MessageRestriction.kt +++ b/domain/src/main/kotlin/com/wespot/user/restriction/MessageRestriction.kt @@ -3,12 +3,13 @@ package com.wespot.user.restriction import com.wespot.exception.CustomException import com.wespot.exception.ExceptionView import com.wespot.user.RestrictionType +import com.wespot.user.restriction.CommunityRestriction.Companion import org.springframework.http.HttpStatus import java.time.LocalDate data class MessageRestriction( - val restrictionType: RestrictionType, - val releaseDate: LocalDate + val restrictionType: RestrictionType = RestrictionType.NONE, + val releaseDate: LocalDate = PERMANENT_BAN_DATE ) { companion object { @@ -18,11 +19,16 @@ data class MessageRestriction( private const val FIRST_MESSAGE_USAGE_RESTRICTION_DAY = 30L private const val SECOND_MESSAGE_USAGE_RESTRICTION_DAY = 90L + private val RESTRICTION_RULE: List = listOf( + RestrictionRule(20, PERMANENT_BAN_DAY), + RestrictionRule(15, 15), + RestrictionRule(10, 7), + RestrictionRule(5, 3), + RestrictionRule(0, 0), + ) + fun createInitialState() = - MessageRestriction( - restrictionType = RestrictionType.NONE, - releaseDate = PERMANENT_BAN_DATE - ) + MessageRestriction() fun of(restrictionType: RestrictionType, restrictionDay: Long): MessageRestriction { validate(restrictionType, restrictionDay) @@ -67,4 +73,26 @@ data class MessageRestriction( fun isKeepRestriction() = restrictionType != RestrictionType.NONE + fun receivedNewReport(reportCount: Long): MessageRestriction { + val restrictionDay = RESTRICTION_RULE + .firstOrNull { reportCount >= it.standard } + ?.restrictionDay ?: 0L + + if (restrictionDay == 0L) { + return this + } + + if (restrictionDay == PERMANENT_BAN_DAY) { + return MessageRestriction( + RestrictionType.PERMANENT_BAN_MESSAGE_REPORT, + PERMANENT_BAN_DATE + ) + } + + return MessageRestriction( + RestrictionType.TEMPORARY_BAN_MESSAGE_REPORT, + LocalDate.now().plusDays(restrictionDay) + ) + } + } diff --git a/domain/src/main/kotlin/com/wespot/user/restriction/Restriction.kt b/domain/src/main/kotlin/com/wespot/user/restriction/Restriction.kt index 27491b19..5d4b9b1c 100644 --- a/domain/src/main/kotlin/com/wespot/user/restriction/Restriction.kt +++ b/domain/src/main/kotlin/com/wespot/user/restriction/Restriction.kt @@ -7,61 +7,82 @@ import org.springframework.http.HttpStatus import java.time.LocalDate data class Restriction( - val id: Long, - val voteRestriction: VoteRestriction, - val messageRestriction: MessageRestriction + val id: Long = 0L, + val voteRestriction: VoteRestriction = VoteRestriction.createInitialState(), + val messageRestriction: MessageRestriction = MessageRestriction.createInitialState(), + val communityRestriction: CommunityRestriction = CommunityRestriction(), ) { companion object { - fun createInitialState() = - Restriction( - id = 0, - voteRestriction = VoteRestriction.createInitialState(), - messageRestriction = MessageRestriction.createInitialState() - ) + fun createInitialState() = Restriction() } - fun addRestrict(restrictionType: RestrictionType, restrictionDay: Long): Restriction { - validate(restrictionType) + fun receivedNewReport( + restrictionCategory: RestrictionCategory, + reportCount: Long, + ): Restriction { + if (restrictionCategory == RestrictionCategory.VOTE) { + return copy( + voteRestriction = voteRestriction.receivedNewReport(reportCount) + ) + } - if (restrictionType.isVoteRestriction()) { - return Restriction( - id = id, - voteRestriction = VoteRestriction.of(restrictionType, restrictionDay), - messageRestriction = messageRestriction + if (restrictionCategory == RestrictionCategory.MESSAGE) { + return copy( + messageRestriction = messageRestriction.receivedNewReport(reportCount) ) } - return Restriction( - id = id, - voteRestriction = voteRestriction, - messageRestriction = MessageRestriction.of(restrictionType, restrictionDay) + return copy( + communityRestriction = communityRestriction.receivedNewReport(reportCount) ) } - private fun validate(restrictionType: RestrictionType) { - require(restrictionType.isVoteRestriction() || restrictionType.isMessageRestriction()) { + fun addRestrict(restrictionType: RestrictionType, restrictionDay: Long): Restriction { + if (restrictionType == RestrictionType.NONE) { throw CustomException( HttpStatus.INTERNAL_SERVER_ERROR, ExceptionView.TOAST, "RestrictionType.NONE을 추가할 수 없습니다." ) } - } + if (restrictionType.isVoteRestriction()) { + return copy( + voteRestriction = VoteRestriction.of(restrictionType, restrictionDay), + ) + } + + return copy( + messageRestriction = MessageRestriction.of(restrictionType, restrictionDay) + ) + } fun getCurrentRestrictionBasedOnTime(date: LocalDate): Restriction { return Restriction( id = id, voteRestriction = voteRestriction.getCurrentRestrictionBasedOnTime(date), - messageRestriction = messageRestriction.getCurrentRestrictionBasedOnTime(date) + messageRestriction = messageRestriction.getCurrentRestrictionBasedOnTime(date), + communityRestriction = communityRestriction.getCurrentRestrictionBasedOnTime(date) ) } fun isKeepRestriction(): Boolean { - return voteRestriction.isKeepRestriction() || messageRestriction.isKeepRestriction() + return voteRestriction.isKeepRestriction() || messageRestriction.isKeepRestriction() || communityRestriction.isKeepRestriction() + } + + fun canNotUseVoteFeature(): Boolean { + return voteRestriction.isKeepRestriction() + } + + fun canNotUseMessageFeature(): Boolean { + return messageRestriction.isKeepRestriction() + } + + fun canNotUseCommunityFeature(): Boolean { + return communityRestriction.isKeepRestriction() } } diff --git a/domain/src/main/kotlin/com/wespot/user/restriction/RestrictionCategory.kt b/domain/src/main/kotlin/com/wespot/user/restriction/RestrictionCategory.kt new file mode 100644 index 00000000..d9843d12 --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/user/restriction/RestrictionCategory.kt @@ -0,0 +1,9 @@ +package com.wespot.user.restriction + +enum class RestrictionCategory { + + VOTE, + MESSAGE, + COMMUNITY, + +} diff --git a/domain/src/main/kotlin/com/wespot/user/restriction/RestrictionRule.kt b/domain/src/main/kotlin/com/wespot/user/restriction/RestrictionRule.kt new file mode 100644 index 00000000..6d2a83fd --- /dev/null +++ b/domain/src/main/kotlin/com/wespot/user/restriction/RestrictionRule.kt @@ -0,0 +1,7 @@ +package com.wespot.user.restriction + +data class RestrictionRule( + val standard: Long = 0L, + val restrictionDay: Long = 0L, +) { +} diff --git a/domain/src/main/kotlin/com/wespot/user/restriction/VoteRestriction.kt b/domain/src/main/kotlin/com/wespot/user/restriction/VoteRestriction.kt index ad2187ec..d09bd742 100644 --- a/domain/src/main/kotlin/com/wespot/user/restriction/VoteRestriction.kt +++ b/domain/src/main/kotlin/com/wespot/user/restriction/VoteRestriction.kt @@ -3,12 +3,13 @@ package com.wespot.user.restriction import com.wespot.exception.CustomException import com.wespot.exception.ExceptionView import com.wespot.user.RestrictionType +import com.wespot.user.restriction.MessageRestriction.Companion import org.springframework.http.HttpStatus import java.time.LocalDate data class VoteRestriction( - val restrictionType: RestrictionType, - val releaseDate: LocalDate, + val restrictionType: RestrictionType = RestrictionType.NONE, + val releaseDate: LocalDate = PERMANENT_BAN_DATE, ) { companion object { @@ -16,11 +17,14 @@ data class VoteRestriction( private val PERMANENT_BAN_DATE = LocalDate.of(9999, 12, 31) private const val PERMANENT_BAN_DAY = Long.MAX_VALUE - fun createInitialState() = VoteRestriction( - restrictionType = RestrictionType.NONE, - releaseDate = PERMANENT_BAN_DATE + private val RESTRICTION_RULE: List = listOf( + RestrictionRule(15, PERMANENT_BAN_DAY), + RestrictionRule(0, 0), ) + + fun createInitialState() = VoteRestriction() + fun of(restrictionType: RestrictionType, restrictionDay: Long): VoteRestriction { validate(restrictionType, restrictionDay) @@ -58,4 +62,19 @@ data class VoteRestriction( fun isKeepRestriction() = restrictionType != RestrictionType.NONE + fun receivedNewReport(reportCount: Long): VoteRestriction { + val restrictionDay = RESTRICTION_RULE + .firstOrNull { reportCount >= it.standard } + ?.restrictionDay ?: 0L + + if (restrictionDay == 0L) { + return this + } + + return VoteRestriction( + RestrictionType.PERMANENT_BAN_VOTE_REPORT, + PERMANENT_BAN_DATE + ) + } + } diff --git a/domain/src/main/kotlin/com/wespot/view/chip/FilterChip.kt b/domain/src/main/kotlin/com/wespot/view/chip/FilterChip.kt index 870b6c3f..c5fe11ce 100644 --- a/domain/src/main/kotlin/com/wespot/view/chip/FilterChip.kt +++ b/domain/src/main/kotlin/com/wespot/view/chip/FilterChip.kt @@ -2,7 +2,7 @@ package com.wespot.view.chip import com.wespot.common.view.ColorType import com.wespot.common.view.TypographType -import com.wespot.post.PostCategories +import com.wespot.post.PostCategory import com.wespot.view.color.Color import com.wespot.view.icon.IconV2 import com.wespot.view.text.RichTextV2 @@ -43,19 +43,19 @@ data class FilterChip( fun of( id: Long, - eachMajorCategory: PostCategories.EachMajorCategory + postCategory: PostCategory, ): FilterChip { return FilterChip( content = FilterChipContent( id = id, icon = IconV2(url = "", color = Color()), text = RichTextV2( - text = eachMajorCategory.majorCategoryName(), + text = postCategory.name, color = Color(value = ColorType.GRAY600.value), typography = TypographType.BODY06.value, maxLine = 1, ), - target = eachMajorCategory.majorCategoryName() + target = postCategory.name ) ) } diff --git a/domain/src/main/kotlin/com/wespot/view/icon/IconV2.kt b/domain/src/main/kotlin/com/wespot/view/icon/IconV2.kt index cbd30afc..8650520f 100644 --- a/domain/src/main/kotlin/com/wespot/view/icon/IconV2.kt +++ b/domain/src/main/kotlin/com/wespot/view/icon/IconV2.kt @@ -46,7 +46,7 @@ data class IconV2( ) val RIGHT_ARROW = IconV2( - url = "https://dw2d2daekmyur.cloudfront.net/right_arrow.png", + url = "https://dw2d2daekmyur.cloudfront.net/right_arrow.jpg", color = Color(value = ColorType.GRAY600.value) ) diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/auth/RefreshTokenMapper.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/auth/RefreshTokenMapper.kt index c64e0353..a9d764aa 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/auth/RefreshTokenMapper.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/auth/RefreshTokenMapper.kt @@ -2,18 +2,22 @@ package com.wespot.auth import com.wespot.common.BaseEntity import com.wespot.school.SchoolJpaEntity +import com.wespot.user.entity.UserPolicyAgreementJpaEntity import com.wespot.user.mapper.UserMapper object RefreshTokenMapper { fun mapToDomainEntity( refreshTokenJpaEntity: RefreshTokenJpaEntity, - schoolJpaEntity: SchoolJpaEntity + schoolJpaEntity: SchoolJpaEntity, ): RefreshToken { return RefreshToken( id = refreshTokenJpaEntity.id, refreshToken = refreshTokenJpaEntity.refreshToken, - user = UserMapper.mapToDomainEntity(refreshTokenJpaEntity.user, schoolJpaEntity), + user = UserMapper.mapToDomainEntity( + refreshTokenJpaEntity.user, + schoolJpaEntity, + ), createdAt = refreshTokenJpaEntity.baseEntity.createdAt, updatedAt = refreshTokenJpaEntity.baseEntity.updatedAt, expiredAt = refreshTokenJpaEntity.expiredAt diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/lock/ExecutorWithLockAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/lock/ExecutorWithLockAdapter.kt new file mode 100644 index 00000000..d347d012 --- /dev/null +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/lock/ExecutorWithLockAdapter.kt @@ -0,0 +1,56 @@ +package com.wespot.lock + +import com.wespot.exception.GetLockFailException +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class ExecutorWithLockAdapter( + private val jdbcTemplate: JdbcTemplate +) : ExecutorWithLock { + + @Transactional(timeout = 10) + @Retryable( + retryFor = [GetLockFailException::class], + backoff = Backoff(delay = 100, multiplier = 2.0) + ) + override fun execute(task: Runnable, lockKey: LockKey) { + tryWithMysqlLock({ + task.run() + null + }, lockKey.key) + } + + private fun tryWithMysqlLock( + task: () -> RETURN_TYPE, + key: String + ): RETURN_TYPE { + val gotLock: Int = jdbcTemplate.queryForObject( + "SELECT GET_LOCK(?, 0)", + Int::class.java, + key + ) + + if (gotLock == 1) { + try { + return task() + } finally { + jdbcTemplate.queryForObject("SELECT RELEASE_LOCK(?)", Int::class.java, key) + } + } + + throw GetLockFailException("Could not acquire lock for key: $key") + } + + @Transactional(timeout = 10) + @Retryable( + retryFor = [GetLockFailException::class], + backoff = Backoff(delay = 100, multiplier = 2.0) + ) + override fun execute(task: () -> RETURN, lockKey: LockKey): RETURN { + return tryWithMysqlLock(task, lockKey.key) + } +} diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/message/v2/MessageV2Mapper.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/message/v2/MessageV2Mapper.kt index efcf4076..fedb19c4 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/message/v2/MessageV2Mapper.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/message/v2/MessageV2Mapper.kt @@ -5,6 +5,7 @@ import com.wespot.message.MessageContent import com.wespot.school.SchoolJpaEntity import com.wespot.user.User import com.wespot.user.entity.UserJpaEntity +import com.wespot.user.entity.UserPolicyAgreementJpaEntity import com.wespot.user.entity.message.AnonymousProfileJpaEntity import com.wespot.user.mapper.AnonymousProfileMapper import com.wespot.user.mapper.UserMapper @@ -56,7 +57,10 @@ object MessageV2Mapper { anonymousProfileJpaEntity: AnonymousProfileJpaEntity?, ): MessageV2 { val sender = - UserMapper.mapToDomainEntity(userJpaEntity = senderJpaEntity, schoolJpaEntity = senderSchoolJpaEntity) + UserMapper.mapToDomainEntity( + userJpaEntity = senderJpaEntity, + schoolJpaEntity = senderSchoolJpaEntity, + ) val receiver = UserMapper.mapToDomainEntity(userJpaEntity = receiverJpaEntity, schoolJpaEntity = receiverSchoolJpaEntity) val anonymousProfile = diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostAdapter.kt index 2ad7b83b..5751a663 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostAdapter.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostAdapter.kt @@ -57,20 +57,25 @@ class PostAdapter( keyword: String, viewerId: Long?, inquirySize: Long, + blockPostIds: List, cursorId: Long? ): List { val posts = postJpaRepository.searchByTitleAndDescription( pattern = keyword, cursorId = cursorId ?: Long.MAX_VALUE, - limit = inquirySize.toInt() + limit = inquirySize.toInt(), + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, viewerId) } - override fun searchByTitle(title: String, viewerId: Long?): List { - val posts = postJpaRepository.findByTitleContainingOrderByBaseEntityCreatedAtDesc( + override fun searchByTitle( + title: String, viewerId: Long?, blockPostIds: List, + ): List { + val posts = postJpaRepository.findByTitleContainingAndIdNotInOrderByBaseEntityCreatedAtDesc( title = title, + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, viewerId) @@ -78,7 +83,7 @@ class PostAdapter( private fun getCompletePost( postEntities: List, - viewerId: Long? + viewerId: Long?, ): List { val viewer = viewerId?.let { userPort.findById(it) } val userIds = postEntities.map { it.userId }.distinct() @@ -137,9 +142,11 @@ class PostAdapter( override fun searchByDescription( description: String, viewerId: Long?, + blockPostIds: List, ): List { - val posts = postJpaRepository.findAllByDescriptionContainingOrderByBaseEntityCreatedAtDesc( + val posts = postJpaRepository.findAllByDescriptionContainingAndIdNotInOrderByBaseEntityCreatedAtDesc( description = description, + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, viewerId) @@ -152,15 +159,13 @@ class PostAdapter( } override fun findAllByCategoryId( - categoryId: Long, - viewerId: Long?, - inquirySize: Long, - cursorId: Long? + categoryId: Long, viewerId: Long?, inquirySize: Long, cursorId: Long?, blockPostIds: List, ): List { - val posts = postJpaRepository.findByCategoryIdAndIdLessThanOrderByBaseEntityCreatedAtDesc( + val posts = postJpaRepository.findByCategoryIdAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( categoryId = categoryId, cursorId = cursorId ?: Long.MAX_VALUE, - pageable = PageRequest.of(0, inquirySize.toInt()) + pageable = PageRequest.of(0, inquirySize.toInt()), + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, viewerId) @@ -168,22 +173,29 @@ class PostAdapter( override fun findAllByCategoryIdIn( categoryIds: List, - viewerId: Long?, inquirySize: Long, cursorId: Long? + viewerId: Long?, + inquirySize: Long, + blockPostIds: List, + cursorId: Long? ): List { - val posts = postJpaRepository.findByCategoryIdInAndIdLessThanOrderByBaseEntityCreatedAtDesc( + val posts = postJpaRepository.findByCategoryIdInAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( categoryIds = categoryIds, cursorId = cursorId ?: Long.MAX_VALUE, - pageable = PageRequest.of(0, inquirySize.toInt()) + pageable = PageRequest.of(0, inquirySize.toInt()), + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, viewerId) } - override fun findAllByUserId(authorId: Long, inquirySize: Long, cursorId: Long?): List { - val posts = postJpaRepository.findAllByUserIdAndIdLessThanOrderByBaseEntityCreatedAtDesc( + override fun findAllByUserId( + authorId: Long, inquirySize: Long, cursorId: Long?, blockPostIds: List, + ): List { + val posts = postJpaRepository.findAllByUserIdAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( userId = authorId, cursorId = cursorId ?: Long.MAX_VALUE, - pageable = PageRequest.of(0, inquirySize.toInt()) + pageable = PageRequest.of(0, inquirySize.toInt()), + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, authorId) @@ -193,12 +205,14 @@ class PostAdapter( postIds: List, viewerId: Long?, inquirySize: Long, + blockPostIds: List, cursorId: Long? ): List { - val posts = postJpaRepository.findAllByIdInAndIdLessThanOrderByBaseEntityCreatedAtDesc( + val posts = postJpaRepository.findAllByIdInAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( postIds = postIds, cursorId = cursorId ?: Long.MAX_VALUE, - pageable = PageRequest.of(0, inquirySize.toInt()) + pageable = PageRequest.of(0, inquirySize.toInt()), + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) return getCompletePost(posts, viewerId) @@ -207,15 +221,18 @@ class PostAdapter( override fun findAllRecentPost( viewerId: Long?, inquirySize: Long, - cursorId: Long?, + blockPostIds: List, + cursorId: Long? ): List { - val findAllByOrderByBaseEntityCreatedAtDesc = - postJpaRepository.findAllByIdLessThanOrderByBaseEntityCreatedAtDesc( - cursorId = cursorId ?: Long.MAX_VALUE, - pageable = PageRequest.of(0, inquirySize.toInt()) + val realCursorId = cursorId ?: Long.MAX_VALUE + val posts = + postJpaRepository.findAllByIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( + cursorId = realCursorId, + pageable = PageRequest.of(0, inquirySize.toInt()), + blockPostIds = blockPostIds.ifEmpty { listOf(Long.MAX_VALUE) }, ) - return getCompletePost(findAllByOrderByBaseEntityCreatedAtDesc, viewerId) + return getCompletePost(posts, viewerId) } override fun deleteById(id: Long) { diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostBlockAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostBlockAdapter.kt index e5c762a0..e1d99293 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostBlockAdapter.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostBlockAdapter.kt @@ -16,7 +16,7 @@ class PostBlockAdapter( } override fun deleteById(id: Long) { - postBlockJpaRepository.deleteById(id) + postBlockJpaRepository.deleteById(id) } override fun save(postBlock: PostBlock): PostBlock { @@ -29,4 +29,9 @@ class PostBlockAdapter( postBlockJpaRepository.deleteByPostId(postId) } + override fun findAllByUserId(userId: Long): List { + return postBlockJpaRepository.findAllByUserId(userId) + .map { PostBlockMapper.toDomain(it) } + } + } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostCategoryAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostCategoryAdapter.kt index 149c7b2a..e5747b73 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostCategoryAdapter.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/post/adapter/PostCategoryAdapter.kt @@ -29,4 +29,9 @@ class PostCategoryAdapter( .map { PostCategoryMapper.toDomain(it) } } + override fun findAllByName(name: String): List { + return postCategoryJpaRepository.findAllByName(name) + .map { PostCategoryMapper.toDomain(it) } + } + } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostBlockJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostBlockJpaRepository.kt index d0e1f34b..08b9f7df 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostBlockJpaRepository.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostBlockJpaRepository.kt @@ -9,4 +9,6 @@ interface PostBlockJpaRepository : JpaRepository { fun deleteByPostId(postId: Long) + fun findAllByUserId(userId: Long): List + } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostCategoryJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostCategoryJpaRepository.kt index c6ba5b91..a90e6ed0 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostCategoryJpaRepository.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostCategoryJpaRepository.kt @@ -10,4 +10,6 @@ interface PostCategoryJpaRepository : JpaRepository { fun findAlLByMajorCategoryName(majorCategoryName: String): List + fun findAllByName(name: String): List + } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostJpaRepository.kt index cd12497f..aa9a0940 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostJpaRepository.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/post/repository/PostJpaRepository.kt @@ -4,43 +4,50 @@ import com.wespot.post.PostEntity import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param interface PostJpaRepository : JpaRepository { - fun findByTitleContainingOrderByBaseEntityCreatedAtDesc( + fun findByTitleContainingAndIdNotInOrderByBaseEntityCreatedAtDesc( title: String, + blockPostIds: List, ): List - fun findAllByDescriptionContainingOrderByBaseEntityCreatedAtDesc( + fun findAllByDescriptionContainingAndIdNotInOrderByBaseEntityCreatedAtDesc( description: String, + @Param("ids") blockPostIds: List, ): List - fun findByCategoryIdAndIdLessThanOrderByBaseEntityCreatedAtDesc( + fun findByCategoryIdAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( categoryId: Long, + @Param("ids") blockPostIds: List, cursorId: Long, pageable: Pageable - ): List - fun findByCategoryIdInAndIdLessThanOrderByBaseEntityCreatedAtDesc( + fun findByCategoryIdInAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( categoryIds: List, + @Param("ids") blockPostIds: List, cursorId: Long, pageable: Pageable ): List - fun findAllByUserIdAndIdLessThanOrderByBaseEntityCreatedAtDesc( + fun findAllByUserIdAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( userId: Long, + @Param("ids") blockPostIds: List, cursorId: Long, pageable: Pageable ): List - fun findAllByIdInAndIdLessThanOrderByBaseEntityCreatedAtDesc( + fun findAllByIdInAndIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( postIds: List, + @Param("ids") blockPostIds: List, cursorId: Long, pageable: Pageable ): List - fun findAllByIdLessThanOrderByBaseEntityCreatedAtDesc( + fun findAllByIdNotInAndIdLessThanOrderByBaseEntityCreatedAtDesc( + @Param("ids") blockPostIds: List, cursorId: Long, pageable: Pageable ): List @@ -52,6 +59,7 @@ interface PostJpaRepository : JpaRepository { WHERE (title REGEXP :pattern OR description REGEXP :pattern) AND id < :cursorId + AND id NOT IN :blockPostIds ORDER BY created_at DESC LIMIT :limit """, @@ -59,6 +67,7 @@ interface PostJpaRepository : JpaRepository { ) fun searchByTitleAndDescription( pattern: String, + blockPostIds: List, cursorId: Long, limit: Int, ): List diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportJpaRepository.kt index f9848822..f1d3e6c9 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportJpaRepository.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportJpaRepository.kt @@ -6,4 +6,6 @@ interface ReportJpaRepository : JpaRepository { fun findAllByReceiverIdAndReportType(reportedId: Long, reportType: ReportType): List + fun deleteByReportTypeAndTargetIdAndReceiverId(reportType: ReportType, targetId: Long, receiverId: Long) + } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportPersistenceAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportPersistenceAdapter.kt index e912707e..0bca5ccd 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportPersistenceAdapter.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/report/ReportPersistenceAdapter.kt @@ -21,4 +21,8 @@ class ReportPersistenceAdapter( return ReportMapper.toDomainEntity(reportJpaRepository.save(reportJpaEntity)) } + override fun deleteByReportTypeAndTargetIdAndReceiverId(reportType: ReportType, targetId: Long, receiverId: Long) { + reportJpaRepository.deleteByReportTypeAndTargetIdAndReceiverId(reportType, targetId, receiverId) + } + } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPersistenceAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPersistenceAdapter.kt index cb58de75..49c55407 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPersistenceAdapter.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPersistenceAdapter.kt @@ -10,6 +10,7 @@ import com.wespot.user.entity.UserJpaEntity import com.wespot.user.mapper.UserMapper import com.wespot.user.port.out.UserPort import com.wespot.user.repository.UserJpaRepository +import com.wespot.user.repository.UserPolicyAgreementJpaRepository import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus @@ -22,6 +23,7 @@ import java.time.LocalDateTime class UserPersistenceAdapter( private val userJpaRepository: UserJpaRepository, private val schoolJpaRepository: SchoolJpaRepository, + private val userPolicyAgreementJpaRepository: UserPolicyAgreementJpaRepository, ) : UserPort { override fun existsBySchoolIdAndGradeAndClassNumber(schoolId: Long, grade: Int, classNumber: Int): Boolean { @@ -30,7 +32,13 @@ class UserPersistenceAdapter( override fun findByEmail(userEmail: String): User? { return userJpaRepository.findByEmail(userEmail) - ?.let { UserMapper.mapToDomainEntity(userJpaEntity = it, schoolJpaEntity = getBySchool(it)) } + ?.let { + UserMapper.mapToDomainEntity( + userJpaEntity = it, + schoolJpaEntity = getBySchool(it), + userPolicyAgreementJpaEntities = userPolicyAgreementJpaRepository.findAllByUserId(userId = it.id) + ) + } } private fun getBySchool(it: UserJpaEntity): SchoolJpaEntity { @@ -54,6 +62,7 @@ class UserPersistenceAdapter( cursorId: Long?, pageable: Pageable, loginUserId: Long, + blockedUserIds: List, ): List { val searchUsers = userJpaRepository.searchUsers( name = name, @@ -62,7 +71,8 @@ class UserPersistenceAdapter( cursorSchoolTypeOrder = cursorSchoolTypeOrder, cursorId = cursorId, pageable = pageable, - loginUserId = loginUserId + loginUserId = loginUserId, + blockedUserIds = blockedUserIds, ) val schoolMap = getSchoolMapsByUsers(searchUsers) return searchUsers @@ -88,6 +98,7 @@ class UserPersistenceAdapter( cursorSchoolTypeOrder: Int?, cursorId: Long?, loginUserId: Long, + blockedUserIds: List, ): Long { return userJpaRepository.countUsersAfterCursor( name = name, @@ -95,7 +106,8 @@ class UserPersistenceAdapter( cursorSchoolName = cursorSchoolName, cursorSchoolTypeOrder = cursorSchoolTypeOrder, cursorId = cursorId, - loginUserId = loginUserId + loginUserId = loginUserId, + blockedUserIds = blockedUserIds, ) } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPolicyAgreementAdapter.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPolicyAgreementAdapter.kt new file mode 100644 index 00000000..38aa9e2b --- /dev/null +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/adapter/UserPolicyAgreementAdapter.kt @@ -0,0 +1,21 @@ +package com.wespot.user.adapter + +import com.wespot.user.UserPolicyAgreement +import com.wespot.user.mapper.UserPolicyAgreementMapper +import com.wespot.user.port.out.UserPolicyAgreementPort +import com.wespot.user.repository.UserPolicyAgreementJpaRepository +import org.springframework.stereotype.Repository + +@Repository +class UserPolicyAgreementAdapter( + private val userPolicyAgreementJpaRepository: UserPolicyAgreementJpaRepository +) : UserPolicyAgreementPort { + + override fun save(userPolicyAgreement: UserPolicyAgreement): UserPolicyAgreement { + val entity = UserPolicyAgreementMapper.toEntity(userPolicyAgreement) + val savedEntity = userPolicyAgreementJpaRepository.save(entity) + + return UserPolicyAgreementMapper.toDomain(savedEntity) + } + +} diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/RestrictionJpaEntity.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/RestrictionJpaEntity.kt index fd179bf7..7c2a2fac 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/RestrictionJpaEntity.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/RestrictionJpaEntity.kt @@ -31,6 +31,13 @@ data class RestrictionJpaEntity( val messageRestrictionType: RestrictionType, @field: NotNull - val messageReleaseDate: LocalDate + val messageReleaseDate: LocalDate, -) + @Enumerated(EnumType.STRING) + @field: NotNull + val communityRestrictionType: RestrictionType, + + @field: NotNull + val communityReleaseDate: LocalDate, + + ) diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserJpaEntity.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserJpaEntity.kt index 110c3647..3ba00024 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserJpaEntity.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserJpaEntity.kt @@ -2,22 +2,9 @@ package com.wespot.user.entity import com.wespot.common.BaseEntity import com.wespot.user.Gender -import com.wespot.user.RestrictionType import com.wespot.user.Role import com.wespot.user.WithdrawalStatus -import jakarta.persistence.CascadeType -import jakarta.persistence.Embedded -import jakarta.persistence.Entity -import jakarta.persistence.EnumType -import jakarta.persistence.Enumerated -import jakarta.persistence.FetchType -import jakarta.persistence.ForeignKey -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.OneToOne -import jakarta.persistence.Table +import jakarta.persistence.* import org.jetbrains.annotations.NotNull import java.time.LocalDateTime diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserPolicyAgreementJpaEntity.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserPolicyAgreementJpaEntity.kt new file mode 100644 index 00000000..fe9007ea --- /dev/null +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/entity/UserPolicyAgreementJpaEntity.kt @@ -0,0 +1,21 @@ +package com.wespot.user.entity + +import com.wespot.user.PolicyType +import jakarta.persistence.* +import org.jetbrains.annotations.NotNull + +@Entity +@Table(name = "user_policy_agreement") +class UserPolicyAgreementJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @field: NotNull + val userId: Long, + + @Enumerated(EnumType.STRING) + @field: NotNull + val policyType: PolicyType, +) { +} diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/RestrictionMapper.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/RestrictionMapper.kt index 1d0c2ae8..bd042681 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/RestrictionMapper.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/RestrictionMapper.kt @@ -1,6 +1,7 @@ package com.wespot.user.mapper import com.wespot.user.entity.RestrictionJpaEntity +import com.wespot.user.restriction.CommunityRestriction import com.wespot.user.restriction.MessageRestriction import com.wespot.user.restriction.Restriction import com.wespot.user.restriction.VoteRestriction @@ -17,6 +18,10 @@ object RestrictionMapper { messageRestriction = MessageRestriction( restrictionType = restrictionJpaEntity.messageRestrictionType, releaseDate = restrictionJpaEntity.messageReleaseDate + ), + communityRestriction = CommunityRestriction( + restrictionType = restrictionJpaEntity.communityRestrictionType, + releaseDate = restrictionJpaEntity.communityReleaseDate ) ) @@ -26,7 +31,9 @@ object RestrictionMapper { voteRestrictionType = restriction.voteRestriction.restrictionType, voteReleaseDate = restriction.voteRestriction.releaseDate, messageRestrictionType = restriction.messageRestriction.restrictionType, - messageReleaseDate = restriction.messageRestriction.releaseDate + messageReleaseDate = restriction.messageRestriction.releaseDate, + communityRestrictionType = restriction.communityRestriction.restrictionType, + communityReleaseDate = restriction.communityRestriction.releaseDate ) } diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserMapper.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserMapper.kt index 1d321536..2e20efc1 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserMapper.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserMapper.kt @@ -6,11 +6,17 @@ import com.wespot.school.SchoolJpaEntity import com.wespot.school.SchoolMapper import com.wespot.user.User import com.wespot.user.UserIntroduction +import com.wespot.user.UserPolicyAgreement import com.wespot.user.entity.UserJpaEntity +import com.wespot.user.entity.UserPolicyAgreementJpaEntity object UserMapper { - fun mapToDomainEntity(userJpaEntity: UserJpaEntity, schoolJpaEntity: SchoolJpaEntity): User = + fun mapToDomainEntity( + userJpaEntity: UserJpaEntity, + schoolJpaEntity: SchoolJpaEntity, + userPolicyAgreementJpaEntities: List = mutableListOf(), + ): User = User( id = userJpaEntity.id, email = userJpaEntity.email, @@ -30,13 +36,19 @@ object UserMapper { restriction = RestrictionMapper.mapToDomainEntity(userJpaEntity.restriction), createdAt = userJpaEntity.baseEntity.createdAt, updatedAt = userJpaEntity.baseEntity.updatedAt, + userPolicyAgreements = userPolicyAgreementJpaEntities.map { UserPolicyAgreementMapper.toDomain(it) }, + withdrawalStatus = userJpaEntity.withdrawalStatus, withdrawalRequestAt = userJpaEntity.withdrawalRequestAt, withdrawalCancelAt = userJpaEntity.withdrawalCancelAt, withdrawalCompleteAt = userJpaEntity.withdrawalCompleteAt, ) - fun mapToDomainEntity(userJpaEntity: UserJpaEntity, school: School): User = + fun mapToDomainEntity( + userJpaEntity: UserJpaEntity, + school: School, + userPolicyAgreements: List = mutableListOf(), + ): User = User( id = userJpaEntity.id, email = userJpaEntity.email, @@ -56,6 +68,7 @@ object UserMapper { restriction = RestrictionMapper.mapToDomainEntity(userJpaEntity.restriction), createdAt = userJpaEntity.baseEntity.createdAt, updatedAt = userJpaEntity.baseEntity.updatedAt, + userPolicyAgreements = userPolicyAgreements, withdrawalStatus = userJpaEntity.withdrawalStatus, withdrawalRequestAt = userJpaEntity.withdrawalRequestAt, withdrawalCancelAt = userJpaEntity.withdrawalCancelAt, diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserPolicyAgreementMapper.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserPolicyAgreementMapper.kt new file mode 100644 index 00000000..fb7bbb28 --- /dev/null +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/mapper/UserPolicyAgreementMapper.kt @@ -0,0 +1,24 @@ +package com.wespot.user.mapper + +import com.wespot.user.UserPolicyAgreement +import com.wespot.user.entity.UserPolicyAgreementJpaEntity + +object UserPolicyAgreementMapper { + + fun toDomain(entity: UserPolicyAgreementJpaEntity): UserPolicyAgreement { + return UserPolicyAgreement( + id = entity.id, + userId = entity.userId, + policyType = entity.policyType + ) + } + + fun toEntity(domain: UserPolicyAgreement): UserPolicyAgreementJpaEntity { + return UserPolicyAgreementJpaEntity( + id = domain.id, + userId = domain.userId, + policyType = domain.policyType + ) + } + +} diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserJpaRepository.kt index 6c3e0099..3723e88f 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserJpaRepository.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserJpaRepository.kt @@ -44,6 +44,7 @@ interface UserJpaRepository : JpaRepository { LEFT JOIN SchoolJpaEntity s ON u.schoolId = s.id WHERE LOWER(u.name) LIKE LOWER(CONCAT('%', :name, '%')) AND u.id <> :loginUserId + AND u.id NOT IN :blockedUserIds AND ( :cursorId IS NULL OR ( (:cursorName IS NULL OR u.name > :cursorName) OR @@ -77,6 +78,7 @@ interface UserJpaRepository : JpaRepository { @Param("cursorSchoolTypeOrder") cursorSchoolTypeOrder: Int?, @Param("cursorId") cursorId: Long?, @Param("loginUserId") loginUserId: Long, + @Param("blockedUserIds") blockedUserIds: List, pageable: Pageable ): List @@ -86,6 +88,7 @@ interface UserJpaRepository : JpaRepository { FROM UserJpaEntity u LEFT JOIN SchoolJpaEntity s ON u.schoolId = s.id AND u.id <> :loginUserId + AND u.id NOT IN :blockedUserIds WHERE LOWER(u.name) LIKE LOWER(CONCAT('%', :name, '%')) AND ( :cursorId IS NULL OR ( @@ -115,6 +118,7 @@ interface UserJpaRepository : JpaRepository { @Param("cursorSchoolTypeOrder") cursorSchoolTypeOrder: Int?, @Param("cursorId") cursorId: Long?, @Param("loginUserId") loginUserId: Long, + @Param("blockedUserIds") blockedUserIds: List, ): Long fun countBySchoolIdAndGradeAndClassNumber( diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserPolicyAgreementJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserPolicyAgreementJpaRepository.kt new file mode 100644 index 00000000..e786cd07 --- /dev/null +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/repository/UserPolicyAgreementJpaRepository.kt @@ -0,0 +1,10 @@ +package com.wespot.user.repository + +import com.wespot.user.entity.UserPolicyAgreementJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface UserPolicyAgreementJpaRepository : JpaRepository { + + fun findAllByUserId(userId: Long): List + +}