Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ build/reports/
coverage/

# Test file
index.html
index.html
docker-compose.yml
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dependencies {

// 웹소켓 - 사용자 채팅 기능 (게스트-가이드)
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.boot:spring-boot-starter-amqp")

// 레디스 - 캐싱 및 세션 관리
implementation("org.springframework.boot:spring-boot-starter-data-redis")
Expand All @@ -74,6 +75,7 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.springframework.amqp:spring-rabbit-test")
testImplementation("io.mockk:mockk:1.13.12")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import com.back.koreaTravelGuide.common.ApiResponse
import com.back.koreaTravelGuide.domain.userChat.chatmessage.dto.ChatMessageResponse
import com.back.koreaTravelGuide.domain.userChat.chatmessage.dto.ChatMessageSendRequest
import com.back.koreaTravelGuide.domain.userChat.chatmessage.service.ChatMessageService
import com.back.koreaTravelGuide.domain.userChat.chatmessage.usecase.ChatMessagePublisher
import org.springframework.http.ResponseEntity
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
Expand All @@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/api/userchat/rooms")
class ChatMessageController(
private val messageService: ChatMessageService,
private val messagingTemplate: SimpMessagingTemplate,
private val chatMessagePublisher: ChatMessagePublisher,
) {
@GetMapping("/{roomId}/messages")
fun listMessages(
Expand Down Expand Up @@ -49,8 +49,8 @@ class ChatMessageController(
val memberId = senderId ?: throw AccessDeniedException("인증이 필요합니다.")
val saved = messageService.send(roomId, memberId, req.content)
val response = ChatMessageResponse.from(saved)
messagingTemplate.convertAndSend(
"/topic/userchat/$roomId",
chatMessagePublisher.publishUserChat(
roomId,
ApiResponse(msg = "메시지 전송", data = response),
)
return ResponseEntity.status(201).body(ApiResponse(msg = "메시지 전송", data = response))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import com.back.koreaTravelGuide.common.ApiResponse
import com.back.koreaTravelGuide.domain.userChat.chatmessage.dto.ChatMessageResponse
import com.back.koreaTravelGuide.domain.userChat.chatmessage.dto.ChatMessageSendRequest
import com.back.koreaTravelGuide.domain.userChat.chatmessage.service.ChatMessageService
import com.back.koreaTravelGuide.domain.userChat.chatmessage.usecase.ChatMessagePublisher
import org.springframework.messaging.handler.annotation.DestinationVariable
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.Payload
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.security.access.AccessDeniedException
import org.springframework.stereotype.Controller
import java.security.Principal

@Controller
class ChatMessageSocketController(
private val chatMessageService: ChatMessageService,
private val messagingTemplate: SimpMessagingTemplate,
private val chatMessagePublisher: ChatMessagePublisher,
) {
@MessageMapping("/userchat/{roomId}/messages")
fun handleMessage(
Expand All @@ -26,8 +26,8 @@ class ChatMessageSocketController(
val senderId = principal.name.toLongOrNull() ?: throw AccessDeniedException("인증이 필요합니다.")
val saved = chatMessageService.send(roomId, senderId, req.content)
val response = ChatMessageResponse.from(saved)
messagingTemplate.convertAndSend(
"/topic/userchat/$roomId",
chatMessagePublisher.publishUserChat(
roomId,
ApiResponse(msg = "메시지 전송", data = response),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.back.koreaTravelGuide.domain.userChat.chatmessage.usecase

interface ChatMessagePublisher {
fun publishUserChat(
roomId: Long,
payload: Any,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.back.koreaTravelGuide.domain.userChat.chatmessage.usecase

import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component

@Profile("prod")
@Component
class RabbitChatMessagePublisher(
private val rabbitTemplate: RabbitTemplate,
) : ChatMessagePublisher {
override fun publishUserChat(
roomId: Long,
payload: Any,
) {
val routingKey = "userchat.$roomId"
rabbitTemplate.convertAndSend("amq.topic", routingKey, payload)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.back.koreaTravelGuide.domain.userChat.chatmessage.usecase

import org.springframework.context.annotation.Profile
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Component

@Profile("!prod")
@Component
class SimpleChatMessagePublisher(
private val messagingTemplate: SimpMessagingTemplate,
) : ChatMessagePublisher {
override fun publishUserChat(
roomId: Long,
payload: Any,
) {
messagingTemplate.convertAndSend("/topic/userchat/$roomId", payload)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.back.koreaTravelGuide.domain.userChat.stomp

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.amqp.support.converter.MessageConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.messaging.simp.config.ChannelRegistration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer

// userChat에서만 사용할 것 같아서 전역에 두지 않고 userChat 도메인에 두었음

@Profile("prod")
@Configuration
@EnableWebSocketMessageBroker
class UserChatRabbitWebSocketConfig(
private val userChatStompAuthChannelInterceptor: UserChatStompAuthChannelInterceptor,
@Value("\${spring.rabbitmq.host}") private val rabbitHost: String,
@Value("\${spring.rabbitmq.stomp-port}") private val rabbitStompPort: Int,
@Value("\${spring.rabbitmq.username}") private val rabbitUsername: String,
@Value("\${spring.rabbitmq.password}") private val rabbitPassword: String,
) : WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/ws/userchat")
.setAllowedOriginPatterns("*")
.withSockJS()
}

override fun configureMessageBroker(registry: MessageBrokerRegistry) {
registry
.setApplicationDestinationPrefixes("/pub")
.enableStompBrokerRelay("/topic")
.setRelayHost(rabbitHost)
.setRelayPort(rabbitStompPort)
.setClientLogin(rabbitUsername)
.setClientPasscode(rabbitPassword)
.setSystemLogin(rabbitUsername)
.setSystemPasscode(rabbitPassword)
}

override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(userChatStompAuthChannelInterceptor)
}

@Bean
fun rabbitMessageConverter(): MessageConverter = Jackson2JsonMessageConverter()
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.back.koreaTravelGuide.domain.userChat.config
package com.back.koreaTravelGuide.domain.userChat.stomp

import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.messaging.simp.config.ChannelRegistration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer

// userChat에서만 사용할 것 같아서 전역에 두지 않고 userChat 도메인에 두었음

@Profile("!prod")
@Configuration
@EnableWebSocketMessageBroker
class UserChatWebSocketConfig(
class UserChatSimpleWebSocketConfig(
private val userChatStompAuthChannelInterceptor: UserChatStompAuthChannelInterceptor,
) : WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
Expand All @@ -21,8 +21,8 @@ class UserChatWebSocketConfig(
}

override fun configureMessageBroker(registry: MessageBrokerRegistry) {
registry.enableSimpleBroker("/topic")
registry.setApplicationDestinationPrefixes("/pub")
registry.enableSimpleBroker("/topic")
}

override fun configureClientInboundChannel(registration: ChannelRegistration) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.back.koreaTravelGuide.domain.userChat.config
package com.back.koreaTravelGuide.domain.userChat.stomp

import com.back.koreaTravelGuide.common.security.JwtTokenProvider
import org.springframework.messaging.Message
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.back.koreaTravelGuide.domain.ai.tour.service

import kotlin.test.assertEquals
import kotlin.test.assertNull
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

class TourParamsParserTest {
private val parser = TourParamsParser()
Expand Down