From c31880354a119289aaa36760e10f0d8e5e86362e Mon Sep 17 00:00:00 2001 From: beekeeper24 Date: Sun, 28 Sep 2025 21:14:58 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor(be):=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20SSE=20=EC=82=AC?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/userChat/UserChatSseEvents.kt | 34 -------------- .../controller/ChatMessageController.kt | 47 +++++++++++++++++++ .../chatroom/controller/ChatRoomController.kt | 42 +---------------- .../chatroom/service/ChatRoomService.kt | 9 +++- 4 files changed, 55 insertions(+), 77 deletions(-) delete mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/UserChatSseEvents.kt create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageController.kt 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..9896f72 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageController.kt @@ -0,0 +1,47 @@ +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.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/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..f6deb46 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,8 +10,9 @@ 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) + data class CreateRoomRequest(val title: String, val guideId: Long, val userId: Long) @Transactional fun exceptOneToOneRoom( @@ -27,7 +29,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 +43,7 @@ class ChatRoomService( // 예외처리 임시 throw IllegalArgumentException("채팅방 생성자만 삭제할 수 있습니다.") } + messageRepository.deleteByRoomId(roomId) roomRepository.deleteById(roomId) } } From 6f9c7c93b7eb06409378f590ec609f32431a379c Mon Sep 17 00:00:00 2001 From: beekeeper24 Date: Sun, 28 Sep 2025 21:15:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor(be)feat(be):=20WebSocket=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/UserChatWebSocketConfig.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/config/UserChatWebSocketConfig.kt 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") + } +} From 6b4a4024fc724dfb0427b4c71503f14a5044bd7a Mon Sep 17 00:00:00 2001 From: beekeeper24 Date: Sun, 28 Sep 2025 21:17:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat(be)=20:=20WebSocket=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ChatMessageSocketController.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/controller/ChatMessageSocketController.kt 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), + ) + } +} From e76cc51eb8864b378dd05058a364e67d5f043c1b Mon Sep 17 00:00:00 2001 From: beekeeper24 Date: Sun, 28 Sep 2025 21:33:07 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(be)=20:=20Drefactor=C2=A8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatmessage/controller/ChatMessageController.kt | 11 ++++++----- .../userChat/chatroom/service/ChatRoomService.kt | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) 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 index 9896f72..37fad20 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -15,7 +16,7 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api/userchat/rooms") class ChatMessageController( private val msgSvc: ChatMessageService, -// private val messagingTemplate: SimpMessagingTemplate, + private val messagingTemplate: SimpMessagingTemplate, ) { @GetMapping("/{roomId}/messages") fun listMessages( @@ -38,10 +39,10 @@ class ChatMessageController( @RequestBody req: ChatMessageService.SendMessageReq, ): ResponseEntity> { val saved = msgSvc.send(roomId, req) -// messagingTemplate.convertAndSend( -// "/topic/userchat/$roomId", -// ApiResponse(msg = "메시지 전송", data = saved), -// ) + 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/chatroom/service/ChatRoomService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt index f6deb46..5996835 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 @@ -12,7 +12,6 @@ class ChatRoomService( private val roomRepository: ChatRoomRepository, private val messageRepository: ChatMessageRepository, ) { - data class CreateRoomRequest(val title: String, val guideId: Long, val userId: Long) @Transactional fun exceptOneToOneRoom( From 23dd0da947a43101dfb2ba551478124490606a37 Mon Sep 17 00:00:00 2001 From: beekeeper24 Date: Sun, 28 Sep 2025 21:33:40 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat(be)=20:=20Drefactor=C2=A8=C3=A3refacto?= =?UTF-8?q?r=20=C2=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/userChat/chatroom/service/ChatRoomService.kt | 1 - 1 file changed, 1 deletion(-) 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 5996835..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 @@ -12,7 +12,6 @@ class ChatRoomService( private val roomRepository: ChatRoomRepository, private val messageRepository: ChatMessageRepository, ) { - @Transactional fun exceptOneToOneRoom( guideId: Long,