diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/UserChatSseEvents.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/UserChatSseEvents.kt deleted file mode 100644 index 4fb9ac3..0000000 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/UserChatSseEvents.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.back.koreaTravelGuide.domain.userChat - -import org.springframework.stereotype.Component -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter -import java.util.concurrent.ConcurrentHashMap - -// Websocket,Stomp 사용 전 임시로 만들었음 -// 테스트 후 제거 예정 - -@Component -class UserChatSseEvents { - private val emitters = ConcurrentHashMap>() - - fun subscribe(roomId: Long): SseEmitter { - val emitter = SseEmitter(0L) - emitters.computeIfAbsent(roomId) { mutableListOf() }.add(emitter) - emitter.onCompletion { emitters[roomId]?.remove(emitter) } - emitter.onTimeout { emitter.complete() } - return emitter - } - - fun publishNew( - roomId: Long, - lastMessageId: Long, - ) { - emitters[roomId]?.toList()?.forEach { - try { - it.send(SseEmitter.event().name("NEW").data(lastMessageId)) - } catch (_: Exception) { - it.complete() - } - } - } -} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageController.kt new file mode 100644 index 0000000..37fad20 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageController.kt @@ -0,0 +1,48 @@ +package com.back.koreaTravelGuide.domain.userChat.chatmessage.controller + +import com.back.koreaTravelGuide.common.ApiResponse +import com.back.koreaTravelGuide.domain.userChat.chatmessage.service.ChatMessageService +import org.springframework.http.ResponseEntity +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/userchat/rooms") +class ChatMessageController( + private val msgSvc: ChatMessageService, + private val messagingTemplate: SimpMessagingTemplate, +) { + @GetMapping("/{roomId}/messages") + fun listMessages( + @PathVariable roomId: Long, + @RequestParam(required = false) after: Long?, + @RequestParam(defaultValue = "50") limit: Int, + ): ResponseEntity> { + val messages = + if (after == null) { + msgSvc.getlistbefore(roomId, limit) + } else { + msgSvc.getlistafter(roomId, after) + } + return ResponseEntity.ok(ApiResponse(msg = "메시지 조회", data = messages)) + } + + @PostMapping("/{roomId}/messages") + fun sendMessage( + @PathVariable roomId: Long, + @RequestBody req: ChatMessageService.SendMessageReq, + ): ResponseEntity> { + val saved = msgSvc.send(roomId, req) + messagingTemplate.convertAndSend( + "/topic/userchat/$roomId", + ApiResponse(msg = "메시지 전송", data = saved), + ) + return ResponseEntity.status(201).body(ApiResponse(msg = "메시지 전송", data = saved)) + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageSocketController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageSocketController.kt new file mode 100644 index 0000000..e977f0c --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageSocketController.kt @@ -0,0 +1,27 @@ +package com.back.koreaTravelGuide.domain.userChat.chatmessage.controller + +import com.back.koreaTravelGuide.common.ApiResponse +import com.back.koreaTravelGuide.domain.userChat.chatmessage.service.ChatMessageService +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.stereotype.Controller + +@Controller +class ChatMessageSocketController( + private val chatMessageService: ChatMessageService, + private val messagingTemplate: SimpMessagingTemplate, +) { + @MessageMapping("/userchat/{roomId}/messages") + fun handleMessage( + @DestinationVariable roomId: Long, + @Payload req: ChatMessageService.SendMessageReq, + ) { + val saved = chatMessageService.send(roomId, req) + messagingTemplate.convertAndSend( + "/topic/userchat/$roomId", + ApiResponse(msg = "메시지 전송", data = saved), + ) + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt index 432b365..536a68b 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt @@ -1,10 +1,7 @@ package com.back.koreaTravelGuide.domain.userChat.chatroom.controller import com.back.koreaTravelGuide.common.ApiResponse -import com.back.koreaTravelGuide.domain.userChat.UserChatSseEvents -import com.back.koreaTravelGuide.domain.userChat.chatmessage.service.ChatMessageService import com.back.koreaTravelGuide.domain.userChat.chatroom.service.ChatRoomService -import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -12,23 +9,18 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter -// 컨트롤러는 임시로 강사님 스타일 따라서 통합해놓았음. 추후 리팩토링 예정 @RestController @RequestMapping("/api/userchat/rooms") class ChatRoomController( private val roomSvc: ChatRoomService, - private val msgSvc: ChatMessageService, - private val events: UserChatSseEvents, ) { data class StartChatReq(val guideId: Long, val userId: Long) data class DeleteChatReq(val userId: Long) - // MVP: 같은 페어는 방 재사용 + // 같은 페어는 방 재사용 @PostMapping("/start") fun startChat( @RequestBody req: StartChatReq, @@ -50,36 +42,4 @@ class ChatRoomController( fun get( @PathVariable roomId: Long, ) = ResponseEntity.ok(ApiResponse(msg = "채팅방 조회", data = roomSvc.get(roomId))) - - @GetMapping("/{roomId}/messages") - fun listMessages( - @PathVariable roomId: Long, - @RequestParam(required = false) after: Long?, - @RequestParam(defaultValue = "50") limit: Int, - ): ResponseEntity> { - val messages = - if (after == null) { - msgSvc.getlistbefore(roomId, limit) - } else { - msgSvc.getlistafter(roomId, after) - } - return ResponseEntity.ok(ApiResponse(msg = "메시지 조회", data = messages)) - } - - @PostMapping("/{roomId}/messages") - fun sendMessage( - @PathVariable roomId: Long, - @RequestBody req: ChatMessageService.SendMessageReq, - ): ResponseEntity> { - val saved = msgSvc.send(roomId, req) - events.publishNew(roomId, saved.id!!) - return ResponseEntity.status(201).body(ApiResponse(msg = "메시지 전송", data = saved)) - } - - // SSE는 스트림이여서 ApiResponse로 감싸지 않았음 - // WebSocket,Stomp 적용되면 바로 삭제 예정 - @GetMapping("/{roomId}/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - fun subscribe( - @PathVariable roomId: Long, - ): SseEmitter = events.subscribe(roomId) } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt index 75a0965..35e446d 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt @@ -1,5 +1,6 @@ package com.back.koreaTravelGuide.domain.userChat.chatroom.service +import com.back.koreaTravelGuide.domain.userChat.chatmessage.repository.ChatMessageRepository import com.back.koreaTravelGuide.domain.userChat.chatroom.entity.ChatRoom import com.back.koreaTravelGuide.domain.userChat.chatroom.repository.ChatRoomRepository import org.springframework.stereotype.Service @@ -9,9 +10,8 @@ import java.time.Instant @Service class ChatRoomService( private val roomRepository: ChatRoomRepository, + private val messageRepository: ChatMessageRepository, ) { - data class CreateRoomReq(val title: String, val guideId: Long, val userId: Long) - @Transactional fun exceptOneToOneRoom( guideId: Long, @@ -27,7 +27,9 @@ class ChatRoomService( ) } - fun get(roomId: Long): ChatRoom = roomRepository.findById(roomId).orElseThrow { NoSuchElementException("room not found: $roomId") } + fun get(roomId: Long): ChatRoom = + roomRepository.findById(roomId) + .orElseThrow { NoSuchElementException("room not found: $roomId") } @Transactional fun deleteByOwner( @@ -39,6 +41,7 @@ class ChatRoomService( // 예외처리 임시 throw IllegalArgumentException("채팅방 생성자만 삭제할 수 있습니다.") } + messageRepository.deleteByRoomId(roomId) roomRepository.deleteById(roomId) } } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/config/UserChatWebSocketConfig.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/config/UserChatWebSocketConfig.kt new file mode 100644 index 0000000..41e7e71 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/config/UserChatWebSocketConfig.kt @@ -0,0 +1,24 @@ +package com.back.koreaTravelGuide.domain.userChat.config + +import org.springframework.context.annotation.Configuration +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 도메인에 두었음 + +@Configuration +@EnableWebSocketMessageBroker +class UserChatWebSocketConfig : WebSocketMessageBrokerConfigurer { + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/ws/userchat") + .setAllowedOriginPatterns("*") + .withSockJS() + } + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + registry.enableSimpleBroker("/topic") + registry.setApplicationDestinationPrefixes("/pub") + } +}