diff --git a/.gitignore b/.gitignore index 056bc3ba..15c92410 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ out/ .DS_Store /src/test/resources/application.yml /src/main/generated/ +/src/main/resources/application-local.yml +/src/main/resources/application-prod.yml diff --git a/build.gradle b/build.gradle index e7caf51f..7ea325bc 100644 --- a/build.gradle +++ b/build.gradle @@ -144,6 +144,9 @@ dependencies { // MongoDB implementation 'org.springframework.boot:spring-boot-starter-data-mongodb:3.1.8' + + // WebSocket + implementation 'org.springframework.boot:spring-boot-starter-websocket' } // Querydsl diff --git a/src/main/java/com/dnd/gongmuin/chat/controller/ChatMessageController.java b/src/main/java/com/dnd/gongmuin/chat/controller/ChatMessageController.java new file mode 100644 index 00000000..4647425f --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat/controller/ChatMessageController.java @@ -0,0 +1,29 @@ +package com.dnd.gongmuin.chat.controller; + +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.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; + +import com.dnd.gongmuin.chat.dto.ChatMessageRequest; +import com.dnd.gongmuin.chat.dto.ChatMessageResponse; +import com.dnd.gongmuin.chat.service.ChatMessageService; + +import lombok.RequiredArgsConstructor; + +@Controller +@RequiredArgsConstructor +public class ChatMessageController { + + private final ChatMessageService chatMessageService; + + @MessageMapping("/chat-rooms/{chatRoomId}") + @SendTo("/sub/chat-rooms/{chatRoomId}") + public ChatMessageResponse sendMessage( + @DestinationVariable("chatRoomId") Long chatRoomId, + @Payload ChatMessageRequest request + ) { + return chatMessageService.saveChatMessage(request, chatRoomId); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java b/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java index 8f3b1cab..f1c5fe2f 100644 --- a/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java +++ b/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java @@ -1,24 +1,17 @@ package com.dnd.gongmuin.chat.controller; import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.RestController; -import com.dnd.gongmuin.chat.dto.ChatMessageRequest; import com.dnd.gongmuin.chat.dto.ChatMessageResponse; import com.dnd.gongmuin.chat.service.ChatRoomService; import com.dnd.gongmuin.common.dto.PageResponse; -import com.dnd.gongmuin.member.domain.Member; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @Tag(name = "채팅방 API") @@ -28,17 +21,6 @@ public class ChatRoomController { private final ChatRoomService chatRoomService; - @Operation(summary = "채팅방 메시지 등록 임시 API", description = "웹소켓 연결 전 임시 API") - @PostMapping("/api/chat-messages/{chatRoomId}") - public ResponseEntity registerChatMessage( - @PathVariable Long chatRoomId, - @Valid @RequestBody ChatMessageRequest request, - @AuthenticationPrincipal Member member - ) { - chatRoomService.saveChatMessage(request, chatRoomId, member.getId()); - return new ResponseEntity<>(HttpStatus.CREATED); - } - @Operation(summary = "채팅방 메시지 조회 API", description = "채팅방 메시지를 최신순으로 페이징한다.") @GetMapping("/api/chat-messages/{chatRoomId}") public ResponseEntity> getChatMessages( diff --git a/src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java b/src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java index 6b220457..b41bf5ab 100644 --- a/src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java +++ b/src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java @@ -26,8 +26,6 @@ public class ChatMessage { private Boolean isRead; - private String mediaUrl; - private MessageType type; private LocalDateTime createdAt; @@ -36,13 +34,11 @@ private ChatMessage( String content, long chatRoomId, long memberId, - String mediaUrl, MessageType type ) { this.content = content; this.chatRoomId = chatRoomId; this.memberId = memberId; - this.mediaUrl = mediaUrl; this.type = type; this.isRead = false; this.createdAt = LocalDateTime.now(); @@ -52,9 +48,8 @@ public static ChatMessage of( String content, long chatRoomId, long memberId, - String mediaUrl, MessageType type ) { - return new ChatMessage(content, chatRoomId, memberId, mediaUrl, type); + return new ChatMessage(content, chatRoomId, memberId, type); } } diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMapper.java b/src/main/java/com/dnd/gongmuin/chat/dto/ChatMapper.java index 29e2d2c8..e1d2273e 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMapper.java +++ b/src/main/java/com/dnd/gongmuin/chat/dto/ChatMapper.java @@ -16,8 +16,7 @@ public static ChatMessageResponse toChatMessageResponse( chatMessage.getMemberId(), chatMessage.getChatRoomId(), chatMessage.getContent(), - chatMessage.getType().getLabel(), - chatMessage.getMediaUrl() + chatMessage.getType().getLabel() ); } @@ -30,7 +29,6 @@ public static ChatMessage toChatMessage( request.content(), chatRoomId, memberId, - request.mediaUrl(), MessageType.of(request.type()) ); } diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageRequest.java b/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageRequest.java index 9c159d74..3deffea8 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageRequest.java +++ b/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageRequest.java @@ -1,12 +1,15 @@ package com.dnd.gongmuin.chat.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record ChatMessageRequest( @NotBlank(message = "채팅 내용을 입력해주세요.") String content, @NotBlank(message = "채팅 타입을 입력해주세요.") String type, - String mediaUrl + + @NotNull(message = "회원 아이디를 입력해주세요.") + Long memberId ) { } diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageResponse.java b/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageResponse.java index f3cdab5e..cb0a4fb3 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageResponse.java +++ b/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageResponse.java @@ -4,7 +4,6 @@ public record ChatMessageResponse( Long memberId, Long chatRoomId, String content, - String type, - String mediaUrl + String type ) { } diff --git a/src/main/java/com/dnd/gongmuin/chat/service/ChatMessageService.java b/src/main/java/com/dnd/gongmuin/chat/service/ChatMessageService.java new file mode 100644 index 00000000..85a7c092 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat/service/ChatMessageService.java @@ -0,0 +1,32 @@ +package com.dnd.gongmuin.chat.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.gongmuin.chat.domain.ChatMessage; +import com.dnd.gongmuin.chat.dto.ChatMapper; +import com.dnd.gongmuin.chat.dto.ChatMessageRequest; +import com.dnd.gongmuin.chat.dto.ChatMessageResponse; +import com.dnd.gongmuin.chat.repository.ChatMessageRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatMessageService { + + private final ChatMessageRepository chatMessageRepository; + + @Transactional + public ChatMessageResponse saveChatMessage( + ChatMessageRequest request, + Long chatRoomId + ) { + Long memberId = request.memberId(); + ChatMessage chatMessage = chatMessageRepository.save(ChatMapper.toChatMessage(request, chatRoomId, memberId)); + log.info("chatRoomId = {}, memberId= {}, chatMessageId= {}", chatRoomId, memberId, chatMessage.getId()); + return ChatMapper.toChatMessageResponse(chatMessage); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java b/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java index 756dab79..e8e25774 100644 --- a/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java +++ b/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java @@ -6,13 +6,11 @@ import org.springframework.transaction.annotation.Transactional; import com.dnd.gongmuin.chat.dto.ChatMapper; -import com.dnd.gongmuin.chat.dto.ChatMessageRequest; import com.dnd.gongmuin.chat.dto.ChatMessageResponse; import com.dnd.gongmuin.chat.repository.ChatMessageRepository; import com.dnd.gongmuin.common.dto.PageMapper; import com.dnd.gongmuin.common.dto.PageResponse; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @Service @@ -21,15 +19,6 @@ public class ChatRoomService { private final ChatMessageRepository chatMessageRepository; - @Transactional - public void saveChatMessage( - @Valid ChatMessageRequest request, - Long chatRoomId, - Long memberId - ) { - chatMessageRepository.save(ChatMapper.toChatMessage(request, chatRoomId, memberId)); - } - @Transactional(readOnly = true) public PageResponse getChatMessages(Long chatRoomId, Pageable pageable) { Slice responsePage = chatMessageRepository diff --git a/src/main/java/com/dnd/gongmuin/common/config/WebSocketConfig.java b/src/main/java/com/dnd/gongmuin/common/config/WebSocketConfig.java new file mode 100644 index 00000000..6310fb85 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/common/config/WebSocketConfig.java @@ -0,0 +1,25 @@ +package com.dnd.gongmuin.common.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; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/sub"); // 구독 경로 + registry.setApplicationDestinationPrefixes("/pub"); // 발행 경로 + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") // 웹소켓 연결 엔드포인트 + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/src/main/java/com/dnd/gongmuin/security/config/SecurityConfig.java b/src/main/java/com/dnd/gongmuin/security/config/SecurityConfig.java index 628b4249..8c4d30ba 100644 --- a/src/main/java/com/dnd/gongmuin/security/config/SecurityConfig.java +++ b/src/main/java/com/dnd/gongmuin/security/config/SecurityConfig.java @@ -81,7 +81,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring() .requestMatchers( "/error", "/favicon.ico", "/api/auth/temp-signup", "/api/auth/temp-signin", - "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html" + "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/ws/**" ); } @@ -91,7 +91,7 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://gongmuin.netlify.app", - "https://gongmuin.site/", "http://localhost:8080")); + "https://gongmuin.site/", "http://localhost:8080", "/ws/**")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setExposedHeaders(Arrays.asList("Set-Cookie", "Authorization")); diff --git a/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java b/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java index 2af0a823..1756cf5a 100644 --- a/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java @@ -34,7 +34,6 @@ void getChatMessages() throws Exception { .andExpect(jsonPath("$.content[0].memberId").value(chatMessages.get(0).getMemberId())) .andExpect(jsonPath("$.content[0].chatRoomId").value(chatMessages.get(0).getChatRoomId())) .andExpect(jsonPath("$.content[0].content").value(chatMessages.get(0).getContent())) - .andExpect(jsonPath("$.content[0].type").value(chatMessages.get(0).getType().getLabel())) - .andExpect(jsonPath("$.content[0].mediaUrl").value(chatMessages.get(0).getMediaUrl())); + .andExpect(jsonPath("$.content[0].type").value(chatMessages.get(0).getType().getLabel())); } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java index 84fc1a75..e3f9e909 100644 --- a/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java +++ b/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java @@ -14,7 +14,6 @@ public static ChatMessage chatMessage() { "하하", 1L, 1L, - null, MessageType.TEXT ); }