Skip to content

Commit

Permalink
Merge pull request #101 from soma-baekgu/feature/BG-382-chat-alert
Browse files Browse the repository at this point in the history
[BG-382]: 채팅 입력에 대한 알림 (2h / 2h)
  • Loading branch information
GGHDMS authored Aug 28, 2024
2 parents 3d1f6ee + 7438624 commit 140c88e
Show file tree
Hide file tree
Showing 26 changed files with 210 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ kafka_data

.run

/*/src/*/resources/application-test.yaml
/*/src/*/resources/application-test*.yaml
/notification/src/*/resources/firebase/*.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.backgu.amaker.application.chat.service.ChatUserCacheFacadeService
import com.backgu.amaker.application.event.service.EventAssignedUserService
import com.backgu.amaker.application.user.service.UserCacheService
import com.backgu.amaker.application.user.service.UserService
import com.backgu.amaker.infra.jpa.chat.repository.ChatRepository
import com.backgu.amaker.infra.jpa.chat.query.ChatRepository
import com.backgu.amaker.infra.jpa.chat.repository.ChatRoomRepository
import com.backgu.amaker.infra.jpa.chat.repository.ChatRoomUserRepository
import com.backgu.amaker.infra.redis.chat.repository.ChatCacheRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.backgu.amaker.application.chat.service.ChatService
import com.backgu.amaker.application.chat.service.ChatUserCacheFacadeService
import com.backgu.amaker.application.event.service.EventAssignedUserService
import com.backgu.amaker.application.event.service.EventService
import com.backgu.amaker.application.notification.service.NotificationEventService
import com.backgu.amaker.application.user.service.UserService
import com.backgu.amaker.common.exception.BusinessException
import com.backgu.amaker.common.status.StatusCode
Expand All @@ -25,6 +26,7 @@ import com.backgu.amaker.domain.chat.ChatType
import com.backgu.amaker.domain.chat.DefaultChatWithUser
import com.backgu.amaker.domain.event.Event
import com.backgu.amaker.domain.event.EventAssignedUser
import com.backgu.amaker.domain.notifiacation.chat.NewChat
import com.backgu.amaker.domain.user.User
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
Expand All @@ -42,6 +44,7 @@ class ChatFacadeService(
private val eventAssignedUserService: EventAssignedUserService,
private val chatUserCacheFacadeService: ChatUserCacheFacadeService,
private val eventPublisher: ApplicationEventPublisher,
private val notificationEventService: NotificationEventService,
) {
@Transactional
fun createChat(
Expand All @@ -57,6 +60,7 @@ class ChatFacadeService(
chatRoomService.save(chatRoom.updateLastChatId(chat))

eventPublisher.publishEvent(DefaultChatSaveEvent.of(chatRoomId, chat.createDefaultChatWithUser(user)))
notificationEventService.publishNotificationEvent(NewChat.of(user, chat, chatRoom))

return DefaultChatWithUserDto.of(chat, user)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class ChatFacadeServiceTest : IntegrationTest() {
// given
val userId = "test-user-id"
val chatRoom: ChatRoom = fixture.setUp(userId = userId)
val prevChats: List<Chat> = fixture.chatFixture.createPersistedChats(chatRoom.id, userId, 30)
fixture.chatFixture.createPersistedChats(chatRoom.id, userId, 30)
val currentChat: Chat = fixture.chatFixture.createPersistedChat(chatRoom.id, userId, "현재 테스트 메시지")

// when
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.backgu.amaker.domain.notifiacation

import com.backgu.amaker.domain.notifiacation.method.RealTimeNotificationMethod

open class ChatRoomNotification(
val chatRoomId: Long,
override val method: RealTimeNotificationMethod,
) : RealTimeBasedNotification(method) {
override val keyPrefix: String
get() = "CHAT_ROOM"

override val keyValue: String
get() = chatRoomId.toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.backgu.amaker.domain.notifiacation.chat

import com.backgu.amaker.domain.chat.Chat
import com.backgu.amaker.domain.chat.ChatRoom
import com.backgu.amaker.domain.notifiacation.ChatRoomNotification
import com.backgu.amaker.domain.notifiacation.method.TemplateEmailNotificationMethod
import com.backgu.amaker.domain.user.User

class NewChat(
chatRoom: ChatRoom,
method: TemplateEmailNotificationMethod,
) : ChatRoomNotification(
chatRoom.id,
method,
) {
companion object {
private fun buildDetailMessage(
chatRoom: ChatRoom,
chat: Chat,
): String =
"${chatRoom.name}에 새로운 메시지가 도착했습니다.\n" +
"메시지 내용: ${chat.content}"

fun of(
sender: User,
chat: Chat,
chatRoom: ChatRoom,
): NewChat =
NewChat(
chatRoom,
TemplateEmailNotificationMethod(
"${sender.name}님이 보낸 메시지",
chat.content,
buildDetailMessage(chatRoom, chat),
"chat-notification",
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.backgu.amaker.application.chat.service
import com.backgu.amaker.common.exception.BusinessException
import com.backgu.amaker.common.status.StatusCode
import com.backgu.amaker.domain.chat.DefaultChatWithUser
import com.backgu.amaker.infra.jpa.chat.repository.ChatRepository
import com.backgu.amaker.infra.jpa.chat.query.ChatRepository
import org.springframework.stereotype.Service

@Service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,6 @@ class ChatRoomUserService(

fun findAllByChatRoom(chatRoom: ChatRoom): List<ChatRoomUser> =
chatRoomUserRepository.findAllByChatRoomId(chatRoom.id).map { it.toDomain() }

fun findUserIdsByChatRoomId(chatRoomId: Long): List<String> = chatRoomUserRepository.findUserIdsByChatRoomId(chatRoomId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.backgu.amaker.common.exception.BusinessException
import com.backgu.amaker.common.status.StatusCode
import com.backgu.amaker.domain.chat.Chat
import com.backgu.amaker.infra.jpa.chat.entity.ChatEntity
import com.backgu.amaker.infra.jpa.chat.repository.ChatRepository
import com.backgu.amaker.infra.jpa.chat.query.ChatRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@ class UserService(
if (users.isEmpty()) throw BusinessException(StatusCode.USER_NOT_FOUND)
return users
}

fun getByChatRoomId(chatRoomId: Long): List<User> {
val users = userRepository.findByChatRoomId(chatRoomId).map { it.toDomain() }
if (users.isEmpty()) throw BusinessException(StatusCode.USER_NOT_FOUND)
return users
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ class WorkspaceService(
fun getWorkspaceById(workspaceId: Long): Workspace =
workspaceRepository.findByIdOrNull(workspaceId)?.toDomain()
?: throw BusinessException(StatusCode.WORKSPACE_NOT_FOUND)

fun getWorkspaceIdByChatRoomId(chatRoomId: Long): Long =
workspaceRepository.getWorkspaceIdByChatRoomId(chatRoomId)
?: throw BusinessException(StatusCode.WORKSPACE_NOT_FOUND)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
package com.backgu.amaker.infra.jpa.chat.repository

import com.backgu.amaker.infra.jpa.chat.query.ChatWithUserQuery
package com.backgu.amaker.infra.jpa.chat.query

interface ChatQueryRepository {
fun findTopByChatRoomIdLittleThanCursorLimitCountWithUser(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.backgu.amaker.infra.jpa.chat.repository
package com.backgu.amaker.infra.jpa.chat.query

import com.backgu.amaker.infra.jpa.chat.entity.QChatEntity.chatEntity
import com.backgu.amaker.infra.jpa.chat.query.ChatWithUserQuery
import com.backgu.amaker.infra.jpa.chat.query.QChatWithUserQuery
import com.backgu.amaker.infra.jpa.user.entity.QUserEntity.userEntity
import com.querydsl.jpa.impl.JPAQueryFactory

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.backgu.amaker.infra.jpa.chat.query

import com.backgu.amaker.infra.jpa.chat.repository.ChatJpaRepository

interface ChatRepository :
ChatJpaRepository,
ChatQueryRepository

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.backgu.amaker.infra.jpa.chat.repository

import com.backgu.amaker.infra.jpa.chat.entity.ChatRoomUserEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface ChatRoomUserRepository : JpaRepository<ChatRoomUserEntity, Long> {
fun existsByUserIdAndChatRoomId(
Expand All @@ -24,4 +25,7 @@ interface ChatRoomUserRepository : JpaRepository<ChatRoomUserEntity, Long> {
): List<ChatRoomUserEntity>

fun findAllByChatRoomId(chatRoomId: Long): List<ChatRoomUserEntity>

@Query("select cru.userId from ChatRoomUser cru where cru.chatRoomId = :chatRoomId")
fun findUserIdsByChatRoomId(chatRoomId: Long): List<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ interface UserRepository : JpaRepository<UserEntity, String> {

@Query("select u from User u join fetch WorkspaceUser wu on u.id = wu.userId where wu.workspaceId = :workspaceId")
fun findByWorkspaceId(workspaceId: Long): List<UserEntity>

@Query("select u from User u join fetch ChatRoomUser cru on u.id = cru.userId where cru.chatRoomId = :chatRoomId")
fun findByChatRoomId(chatRoomId: Long): List<UserEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ interface WorkspaceRepository : JpaRepository<WorkspaceEntity, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select w from Workspace w where w.id = :id")
fun getLockedWorkspaceById(id: Long): WorkspaceEntity?

@Query("select ch.workspaceId from ChatRoom ch where ch.id = :chatRoomId")
fun getWorkspaceIdByChatRoomId(chatRoomId: Long): Long?
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import org.springframework.scheduling.annotation.EnableAsync
@EnableAsync
@SpringBootApplication
@EntityScan(basePackages = ["com.backgu.amaker.infra"])
@EnableJpaRepositories(basePackages = ["com.backgu.amaker.infra.jpa.user", "com.backgu.amaker.infra.jpa.workspace"])
@EnableJpaRepositories(
basePackages = [
"com.backgu.amaker.infra.jpa.user",
"com.backgu.amaker.infra.jpa.workspace",
"com.backgu.amaker.infra.jpa.chat.repository",
],
)
@EnableRedisRepositories(basePackages = ["com.backgu.amaker.infra.redis"])
class NotificationApplication

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.backgu.amaker.notification.chatroom.config

import com.backgu.amaker.application.chat.service.ChatRoomUserService
import com.backgu.amaker.infra.jpa.chat.repository.ChatRoomUserRepository
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class ChatRoomServiceConfig {
@Bean
fun chatRoomUserService(chatRoomUserRepository: ChatRoomUserRepository) = ChatRoomUserService(chatRoomUserRepository)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ class RealTimeHandler(
private val realTimeCallService: RealTimeCallService,
) {
fun handleUserRealTimeNotification(
userId: String,
userIds: List<String>,
realTimeServer: RealTimeServer,
event: RealTimeBasedNotification,
): List<String> = realTimeCallService.sendUserRealTimeNotification(listOf(userId), realTimeServer, event)
): List<String> = realTimeCallService.sendUserRealTimeNotification(userIds, realTimeServer, event)

fun handlerWorkspaceRealTimeNotification(
workspaceId: Long,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.backgu.amaker.notification.service.adapter

import com.backgu.amaker.application.chat.service.ChatRoomUserService
import com.backgu.amaker.application.user.service.UserDeviceService
import com.backgu.amaker.application.workspace.WorkspaceService
import com.backgu.amaker.domain.notifiacation.ChatRoomNotification
import com.backgu.amaker.domain.notifiacation.Notification
import com.backgu.amaker.domain.notifiacation.method.NotificationMethod
import com.backgu.amaker.domain.notifiacation.method.RealTimeNotificationMethod
import com.backgu.amaker.domain.realtime.RealTimeServer
import com.backgu.amaker.domain.session.Session
import com.backgu.amaker.notification.realtime.handler.RealTimeHandler
import com.backgu.amaker.notification.realtime.service.RealTimeService
import com.backgu.amaker.notification.workspace.service.WorkspaceSessionService
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component

@Component
class RealTimeChatRoomHandlerAdapter(
private val realTimeHandler: RealTimeHandler,
private val realTimeService: RealTimeService,
private val workspaceSessionService: WorkspaceSessionService,
private val workspaceService: WorkspaceService,
private val chatRoomUserService: ChatRoomUserService,
private val userDeviceService: UserDeviceService,
private val applicationEventPublisher: ApplicationEventPublisher,
) : NotificationHandlerAdapter<ChatRoomNotification, RealTimeNotificationMethod> {
override fun supportsNotification(notification: Notification): Boolean = notification is ChatRoomNotification

override fun supportsMethod(method: NotificationMethod): Boolean = method is RealTimeNotificationMethod

override fun process(
notification: ChatRoomNotification,
method: RealTimeNotificationMethod,
) {
val workspaceId: Long = workspaceService.getWorkspaceIdByChatRoomId(notification.chatRoomId)
val sessions: List<Session> = workspaceSessionService.findByWorkspaceId(workspaceId)
val realTimeServerSet: Set<RealTimeServer> =
realTimeService.findByIdsToSet(sessions.mapTo(mutableSetOf()) { it.realtimeId })

val chatRoomUsers = chatRoomUserService.findUserIdsByChatRoomId(notification.chatRoomId)
val successUsers: List<String> =
realTimeServerSet
.map {
realTimeHandler.handleUserRealTimeNotification(chatRoomUsers, it, notification)
}.flatten()

val failedUsers: List<String> = chatRoomUsers.filterNot { it in successUsers }

val pushNotification = notification.toPushNotification(userDeviceService.findByUserIds(failedUsers))
applicationEventPublisher.publishEvent(pushNotification)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RealTimeUserHandlerAdapter(
realTimeService.findByIdsToSet(sessions.mapTo(mutableSetOf()) { it.realtimeId })
val successUser =
realTimeServerSet.map {
realTimeHandler.handleUserRealTimeNotification(notification.userId, it, notification)
realTimeHandler.handleUserRealTimeNotification(listOf(notification.userId), it, notification)
}

if (successUser.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.backgu.amaker.notification.service.adapter

import com.backgu.amaker.application.user.service.UserService
import com.backgu.amaker.domain.notifiacation.ChatRoomNotification
import com.backgu.amaker.domain.notifiacation.Notification
import com.backgu.amaker.domain.notifiacation.method.NotificationMethod
import com.backgu.amaker.domain.notifiacation.method.TemplateEmailNotificationMethod
import com.backgu.amaker.notification.email.service.TemplateEmailHandler
import org.springframework.stereotype.Component

@Component
class TemplateEmailChatRoomHandlerAdapter(
private val userService: UserService,
private val templateEmailHandler: TemplateEmailHandler,
) : NotificationHandlerAdapter<ChatRoomNotification, TemplateEmailNotificationMethod> {
override fun supportsNotification(notification: Notification): Boolean = notification is ChatRoomNotification

override fun supportsMethod(method: NotificationMethod): Boolean = method is TemplateEmailNotificationMethod

override fun process(
notification: ChatRoomNotification,
method: TemplateEmailNotificationMethod,
) {
val users = userService.getByChatRoomId(notification.chatRoomId)
users.forEach { templateEmailHandler.handleEmailEvent(it, method) }
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.backgu.amaker.notification.workspace.config

import com.backgu.amaker.application.workspace.WorkspaceService
import com.backgu.amaker.application.workspace.WorkspaceUserService
import com.backgu.amaker.infra.jpa.workspace.repository.WorkspaceRepository
import com.backgu.amaker.infra.jpa.workspace.repository.WorkspaceUserRepository
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class WorkspaceServiceConfig {
@Bean
fun workspaceService(workspaceRepository: WorkspaceRepository): WorkspaceService = WorkspaceService(workspaceRepository)

@Bean
fun workspaceUserService(workspaceUserRepository: WorkspaceUserRepository): WorkspaceUserService =
WorkspaceUserService(workspaceUserRepository)
Expand Down
10 changes: 10 additions & 0 deletions notification/src/main/resources/templates/chat-notification.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>새로운 채팅</title>
</head>
<body>
<h1 th:text="${title}"></h1>
<p th:text="${content}"></p>
</body>
</html>

0 comments on commit 140c88e

Please sign in to comment.