From 9dcc3bd1f0096238c332b24919b045274c58872d Mon Sep 17 00:00:00 2001 From: bingseok Date: Sat, 7 Dec 2024 17:58:56 +0900 Subject: [PATCH 1/2] [feat] websocket + stomp --- build.gradle | 5 ++ .../B1G4/bookmark/config/WebSocketConfig.java | 27 +++++++++++ .../java/B1G4/bookmark/domain/ChatRoom.java | 18 +++++++ .../repository/ChatRoomRepository.java | 7 +++ .../ChatRoomService/ChatRoomService.java | 4 ++ .../ChatRoomService/ChatRoomServiceImpl.java | 48 +++++++++++++++++++ .../web/controller/ChatController.java | 39 +++++++++++++++ .../dto/ChatMessageDTO/ChatMessageDTO.java | 18 +++++++ 8 files changed, 166 insertions(+) create mode 100644 src/main/java/B1G4/bookmark/config/WebSocketConfig.java create mode 100644 src/main/java/B1G4/bookmark/domain/ChatRoom.java create mode 100644 src/main/java/B1G4/bookmark/repository/ChatRoomRepository.java create mode 100644 src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomService.java create mode 100644 src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomServiceImpl.java create mode 100644 src/main/java/B1G4/bookmark/web/controller/ChatController.java create mode 100644 src/main/java/B1G4/bookmark/web/dto/ChatMessageDTO/ChatMessageDTO.java diff --git a/build.gradle b/build.gradle index 1438524..0facfd4 100644 --- a/build.gradle +++ b/build.gradle @@ -56,8 +56,13 @@ dependencies { //prometheus implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + + // websocket + stomp + implementation 'org.springframework.boot:spring-boot-starter-websocket' + } + tasks.named('test') { useJUnitPlatform() } diff --git a/src/main/java/B1G4/bookmark/config/WebSocketConfig.java b/src/main/java/B1G4/bookmark/config/WebSocketConfig.java new file mode 100644 index 0000000..25d18fb --- /dev/null +++ b/src/main/java/B1G4/bookmark/config/WebSocketConfig.java @@ -0,0 +1,27 @@ +package B1G4.bookmark.config; + +import lombok.RequiredArgsConstructor; +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; + +@Configuration +@RequiredArgsConstructor +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic"); // 구독 경로 + config.setApplicationDestinationPrefixes("/app"); // 클라이언트 발행 경로 + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") // WebSocket 엔드포인트 + .setAllowedOriginPatterns("*") // 모든 Origin 허용 + .withSockJS(); // SockJS Fallback 지원 + } +} \ No newline at end of file diff --git a/src/main/java/B1G4/bookmark/domain/ChatRoom.java b/src/main/java/B1G4/bookmark/domain/ChatRoom.java new file mode 100644 index 0000000..194f2d0 --- /dev/null +++ b/src/main/java/B1G4/bookmark/domain/ChatRoom.java @@ -0,0 +1,18 @@ +package B1G4.bookmark.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class ChatRoom { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/B1G4/bookmark/repository/ChatRoomRepository.java b/src/main/java/B1G4/bookmark/repository/ChatRoomRepository.java new file mode 100644 index 0000000..9929c8d --- /dev/null +++ b/src/main/java/B1G4/bookmark/repository/ChatRoomRepository.java @@ -0,0 +1,7 @@ +package B1G4.bookmark.repository; + +import B1G4.bookmark.domain.ChatRoom; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatRoomRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomService.java b/src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomService.java new file mode 100644 index 0000000..3cbe120 --- /dev/null +++ b/src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomService.java @@ -0,0 +1,4 @@ +package B1G4.bookmark.service.ChatRoomService; + +public interface ChatRoomService { +} diff --git a/src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomServiceImpl.java b/src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomServiceImpl.java new file mode 100644 index 0000000..cb39226 --- /dev/null +++ b/src/main/java/B1G4/bookmark/service/ChatRoomService/ChatRoomServiceImpl.java @@ -0,0 +1,48 @@ +package B1G4.bookmark.service.ChatRoomService; + +import B1G4.bookmark.domain.Member; +import B1G4.bookmark.repository.ChatRoomRepository; +import B1G4.bookmark.repository.MemberRepository; +import B1G4.bookmark.web.dto.ChatMessageDTO.ChatMessageDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class ChatRoomServiceImpl { + + + private final ChatRoomRepository chatRoomRepository; + private final MemberRepository memberRepository; + + // 채팅방 입장 처리 + public ChatMessageDTO handleJoinChatRoom(Long chatRoomId, Long senderId) { + // 채팅방 유효성 확인 + chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new IllegalArgumentException("ChatRoom not found")); + + // 사용자 확인 및 닉네임 설정 + Member member = memberRepository.findById(senderId) + .orElseThrow(() -> new IllegalArgumentException("Member not found")); + + String nickname = member.getNickname(); + String message = nickname + " 님이 입장했습니다."; + LocalDateTime timestamp = LocalDateTime.now(); + + return new ChatMessageDTO(nickname, message, timestamp); + } + + // 메시지 전송 처리 + public ChatMessageDTO handleSendMessage(Long chatRoomId, ChatMessageDTO messageDTO) { + // 채팅방 유효성 확인 + chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new IllegalArgumentException("ChatRoom not found")); + + // 메시지에 타임스탬프 추가 + messageDTO.setTimestamp(LocalDateTime.now()); + + return messageDTO; + } +} \ No newline at end of file diff --git a/src/main/java/B1G4/bookmark/web/controller/ChatController.java b/src/main/java/B1G4/bookmark/web/controller/ChatController.java new file mode 100644 index 0000000..a6dc933 --- /dev/null +++ b/src/main/java/B1G4/bookmark/web/controller/ChatController.java @@ -0,0 +1,39 @@ +package B1G4.bookmark.web.controller; + + +import B1G4.bookmark.domain.Member; +import B1G4.bookmark.security.handler.annotation.AuthUser; +import B1G4.bookmark.service.ChatRoomService.ChatRoomServiceImpl; +import B1G4.bookmark.web.dto.ChatMessageDTO.ChatMessageDTO; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; + +@Controller +@RequiredArgsConstructor +public class ChatController { + + private final ChatRoomServiceImpl chatRoomService; + + // 채팅방 입장 + @MessageMapping("/chat/{chatRoomId}/join") + @SendTo("/topic/chat/{chatRoomId}") + public ChatMessageDTO joinChatRoom( + @DestinationVariable Long chatRoomId, + @Parameter(name = "user", hidden = true) + @AuthUser Member member) { + return chatRoomService.handleJoinChatRoom(chatRoomId, member.getId()); + } + + // 메시지 전송 + @MessageMapping("/chat/{chatRoomId}/send") + @SendTo("/topic/chat/{chatRoomId}") + public ChatMessageDTO sendMessage( + @DestinationVariable Long chatRoomId, + ChatMessageDTO messageDTO) { + return chatRoomService.handleSendMessage(chatRoomId, messageDTO); + } +} \ No newline at end of file diff --git a/src/main/java/B1G4/bookmark/web/dto/ChatMessageDTO/ChatMessageDTO.java b/src/main/java/B1G4/bookmark/web/dto/ChatMessageDTO/ChatMessageDTO.java new file mode 100644 index 0000000..5341f7a --- /dev/null +++ b/src/main/java/B1G4/bookmark/web/dto/ChatMessageDTO/ChatMessageDTO.java @@ -0,0 +1,18 @@ +package B1G4.bookmark.web.dto.ChatMessageDTO; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ChatMessageDTO { + private String nickname; // 보낸 사용자 닉네임 + private String message; // 메시지 내용 + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime timestamp; // 메시지 타임스탬프 +} \ No newline at end of file From fcf1575e6af2ebeb50da93b93f57ceb69e497c48 Mon Sep 17 00:00:00 2001 From: bingseok Date: Sat, 7 Dec 2024 18:09:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[add]=20swagger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/ChatController.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/java/B1G4/bookmark/web/controller/ChatController.java b/src/main/java/B1G4/bookmark/web/controller/ChatController.java index a6dc933..56fdcd8 100644 --- a/src/main/java/B1G4/bookmark/web/controller/ChatController.java +++ b/src/main/java/B1G4/bookmark/web/controller/ChatController.java @@ -5,6 +5,7 @@ import B1G4.bookmark.security.handler.annotation.AuthUser; import B1G4.bookmark.service.ChatRoomService.ChatRoomServiceImpl; import B1G4.bookmark.web.dto.ChatMessageDTO.ChatMessageDTO; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -19,20 +20,50 @@ public class ChatController { private final ChatRoomServiceImpl chatRoomService; // 채팅방 입장 + /** + * 채팅방 입장 + * 클라이언트는 /app/chat/{chatRoomId}/join으로 메시지를 발행합니다. + * 서버는 /topic/chat/{chatRoomId}로 구독 중인 모든 클라이언트에게 메시지를 브로드캐스트합니다. + */ + @Operation( + summary = "채팅방 입장", + description = "사용자가 특정 채팅방에 입장합니다. 입장 메시지는 구독 중인 모든 사용자에게 전송됩니다." + ) @MessageMapping("/chat/{chatRoomId}/join") @SendTo("/topic/chat/{chatRoomId}") public ChatMessageDTO joinChatRoom( - @DestinationVariable Long chatRoomId, + @DestinationVariable @Parameter( + description = "입장할 채팅방의 ID", + required = true, + example = "1" + ) Long chatRoomId, @Parameter(name = "user", hidden = true) @AuthUser Member member) { return chatRoomService.handleJoinChatRoom(chatRoomId, member.getId()); } // 메시지 전송 + /** + * 메시지 전송 + * 클라이언트는 /app/chat/{chatRoomId}/send로 메시지를 발행합니다. + * 서버는 /topic/chat/{chatRoomId}로 구독 중인 모든 클라이언트에게 메시지를 브로드캐스트합니다. + */ + @Operation( + summary = "메시지 전송", + description = "사용자가 특정 채팅방에 메시지를 전송합니다. 메시지는 구독 중인 모든 사용자에게 브로드캐스트됩니다." + ) @MessageMapping("/chat/{chatRoomId}/send") @SendTo("/topic/chat/{chatRoomId}") public ChatMessageDTO sendMessage( - @DestinationVariable Long chatRoomId, + @DestinationVariable @Parameter( + description = "메시지를 전송할 채팅방의 ID", + required = true, + example = "1" + ) Long chatRoomId, + @Parameter( + description = "전송할 메시지의 정보 (메시지만 포함)", + required = true + ) ChatMessageDTO messageDTO) { return chatRoomService.handleSendMessage(chatRoomId, messageDTO); }