diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java index d9f69098d..33e39b8dc 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java @@ -2,14 +2,20 @@ import com.ddang.ddang.chat.application.MessageService; import com.ddang.ddang.chat.application.dto.CreateMessageDto; +import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.domain.Message; import com.ddang.ddang.chat.domain.WebSocketChatSessions; import com.ddang.ddang.chat.handler.dto.ChatMessageDataDto; +import com.ddang.ddang.chat.handler.dto.ChatPingDataDto; +import com.ddang.ddang.chat.handler.dto.HandleMessageResponse; import com.ddang.ddang.chat.handler.dto.MessageDto; +import com.ddang.ddang.chat.handler.dto.SendMessageStatus; import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest; +import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; import com.ddang.ddang.websocket.handler.WebSocketHandleTextMessageProvider; +import com.ddang.ddang.websocket.handler.dto.ChattingType; import com.ddang.ddang.websocket.handler.dto.SendMessageDto; import com.ddang.ddang.websocket.handler.dto.SessionAttributeDto; import com.ddang.ddang.websocket.handler.dto.TextMessageType; @@ -30,6 +36,9 @@ @RequiredArgsConstructor public class ChatWebSocketHandleTextMessageProvider implements WebSocketHandleTextMessageProvider { + private static final String CHATROOM_ID_KEY = "chatRoomId"; + private static final String CHATTTING_TYPE_KEY = "type"; + private final WebSocketChatSessions sessions; private final ObjectMapper objectMapper; private final MessageService messageService; @@ -47,15 +56,18 @@ public List handleCreateSendMessage( final Map data ) throws JsonProcessingException { final SessionAttributeDto sessionAttribute = getSessionAttributes(session); - final ChatMessageDataDto messageData = objectMapper.convertValue(data, ChatMessageDataDto.class); - sessions.add(session, messageData.chatRoomId()); + final long chatRoomId = getChatRoomId(data); + sessions.add(session, chatRoomId); - final Long writerId = sessionAttribute.userId(); - final CreateMessageDto createMessageDto = createMessageDto(messageData, writerId); - final Message message = messageService.create(createMessageDto); - sendNotificationIfReceiverNotInSession(message, sessionAttribute); + final ChattingType type = ChattingType.findValue(data.get(CHATTTING_TYPE_KEY)); + if (ChattingType.PING == type) { + return createPingResponse(sessionAttribute, data, session); + } + return createSendMessageResponse(data, sessionAttribute); + } - return createSendMessages(message, writerId, createMessageDto.chatRoomId()); + private long getChatRoomId(final Map data) { + return Long.parseLong(data.get(CHATROOM_ID_KEY)); } private SessionAttributeDto getSessionAttributes(final WebSocketSession session) { @@ -64,6 +76,32 @@ private SessionAttributeDto getSessionAttributes(final WebSocketSession session) return objectMapper.convertValue(attributes, SessionAttributeDto.class); } + private List createPingResponse(final SessionAttributeDto sessionAttribute, final Map data, final WebSocketSession userSession) throws JsonProcessingException { + final ChatPingDataDto pingData = objectMapper.convertValue(data, ChatPingDataDto.class); + final ReadMessageRequest readMessageRequest = new ReadMessageRequest(sessionAttribute.userId(), pingData.chatRoomId(), pingData.lastMessageId()); + final List readMessageDtos = messageService.readAllByLastMessageId(readMessageRequest); + + final List messageDtos = convertToMessageDto(readMessageDtos, userSession); + final HandleMessageResponse handleMessageResponse = new HandleMessageResponse(SendMessageStatus.SUCCESS, messageDtos); + return List.of(new SendMessageDto(userSession, new TextMessage(objectMapper.writeValueAsString(handleMessageResponse)))); + } + + private List convertToMessageDto(final List readMessageDtos, final WebSocketSession session) { + return readMessageDtos.stream() + .map(readMessageDto -> MessageDto.of(readMessageDto, isMyMessage(session, readMessageDto.writerId()))) + .toList(); + } + + private List createSendMessageResponse(final Map data, final SessionAttributeDto sessionAttribute) throws JsonProcessingException { + final Long writerId = sessionAttribute.userId(); + final ChatMessageDataDto messageData = objectMapper.convertValue(data, ChatMessageDataDto.class); + final CreateMessageDto createMessageDto = createMessageDto(messageData, writerId); + final Message message = messageService.create(createMessageDto); + sendNotificationIfReceiverNotInSession(message, sessionAttribute); + + return createSendMessages(message, writerId, createMessageDto.chatRoomId()); + } + private CreateMessageDto createMessageDto(final ChatMessageDataDto messageData, final Long userId) { final CreateMessageRequest request = new CreateMessageRequest( messageData.receiverId(), @@ -95,7 +133,8 @@ private List createSendMessages( final List sendMessageDtos = new ArrayList<>(); for (final WebSocketSession currentSession : groupSessions) { - final TextMessage textMessage = createTextMessage(message, writerId, currentSession); + final MessageDto messageDto = MessageDto.of(message, isMyMessage(currentSession, writerId)); + final TextMessage textMessage = createTextMessage(messageDto); sendMessageDtos.add(new SendMessageDto(currentSession, textMessage)); updateReadMessageLog(currentSession, chatRoomId, message); } @@ -104,14 +143,11 @@ private List createSendMessages( } private TextMessage createTextMessage( - final Message message, - final Long writerId, - final WebSocketSession session + final MessageDto messageDto ) throws JsonProcessingException { - final boolean isMyMessage = isMyMessage(session, writerId); - final MessageDto messageDto = MessageDto.of(message, isMyMessage); + final HandleMessageResponse handleMessageResponse = new HandleMessageResponse(SendMessageStatus.SUCCESS, List.of(messageDto)); - return new TextMessage(objectMapper.writeValueAsString(messageDto)); + return new TextMessage(objectMapper.writeValueAsString(handleMessageResponse)); } private boolean isMyMessage(final WebSocketSession session, final Long writerId) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatPingDataDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatPingDataDto.java new file mode 100644 index 000000000..845982b30 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatPingDataDto.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.chat.handler.dto; + +public record ChatPingDataDto(Long chatRoomId, Long lastMessageId) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/HandleMessageResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/HandleMessageResponse.java new file mode 100644 index 000000000..d624e782e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/HandleMessageResponse.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.chat.handler.dto; + +import java.util.List; + +public record HandleMessageResponse(SendMessageStatus sendMessageStatus, List messages) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java index 30e7e5512..4cb12590c 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java @@ -1,5 +1,6 @@ package com.ddang.ddang.chat.handler.dto; +import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.domain.Message; import com.fasterxml.jackson.annotation.JsonFormat; @@ -24,4 +25,13 @@ public static MessageDto of(final Message message, final boolean isMyMessage) { message.getContents() ); } + + public static MessageDto of(final ReadMessageDto readMessageDto, final boolean isMyMessage) { + return new MessageDto( + readMessageDto.id(), + readMessageDto.createdTime(), + isMyMessage, + readMessageDto.contents() + ); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendMessageStatus.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendMessageStatus.java new file mode 100644 index 000000000..0a7b7ba1f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendMessageStatus.java @@ -0,0 +1,9 @@ +package com.ddang.ddang.chat.handler.dto; + +public enum SendMessageStatus { + + SUCCESS, + DISCONNECTED, + FORBIDDEN, + ; +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/ChattingType.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/ChattingType.java new file mode 100644 index 000000000..92d62a35b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/ChattingType.java @@ -0,0 +1,23 @@ +package com.ddang.ddang.websocket.handler.dto; + +import java.util.Arrays; + +public enum ChattingType { + + MESSAGE("message"), + PING("ping"), + ; + + private String value; + + ChattingType(final String value) { + this.value = value; + } + + public static ChattingType findValue(final String value) { + return Arrays.stream(ChattingType.values()) + .filter(chattingType -> chattingType.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("잘못된 채팅 타입입니다.")); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java index dd7c99cd9..ef8873bf1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java @@ -28,6 +28,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; @@ -173,6 +174,19 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText assertThat(actual).hasSize(1); } + @Test + void 잘못된_데이터_타입_전달시_예외가_발생한다() throws JsonProcessingException { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(false).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + + // when + assertThatThrownBy(() -> provider.handleCreateSendMessage(writerSession, 잘못된_메시지_전송_데이터)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test void 세션을_삭제한다() { // given diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java index 3d9d288ac..a1fc31506 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java @@ -10,7 +10,6 @@ import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; -import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.user.domain.Reliability; import com.ddang.ddang.user.domain.User; @@ -47,6 +46,7 @@ public class ChatWebSocketHandleTextMessageProviderTestFixture { protected Map 발신자_세션_attribute_정보; protected Map 수신자_세션_attribute_정보; protected Map 메시지_전송_데이터; + protected Map 잘못된_메시지_전송_데이터; protected CreateReadMessageLogEvent 메시지_로그_생성_이벤트; @@ -89,10 +89,14 @@ void setUpFixture() { 발신자_세션_attribute_정보 = new HashMap<>(Map.of("userId", 발신자.getId(), "baseUrl", "/images")); 수신자_세션_attribute_정보 = new HashMap<>(Map.of("userId", 수신자.getId(), "baseUrl", "/images")); 메시지_전송_데이터 = Map.of( + "type", "message", "chatRoomId", String.valueOf(채팅방.getId()), "receiverId", String.valueOf(수신자.getId()), "contents", "메시지 내용" ); + 잘못된_메시지_전송_데이터 = Map.of( + "type", "wrong message type" + ); 메시지_로그_생성_이벤트 = new CreateReadMessageLogEvent(채팅방); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java index 847fbcb40..5f1479c00 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java @@ -68,42 +68,42 @@ public class NotificationEventListenerFixture { @BeforeEach void setUpFixture() { final User 발신자_겸_판매자 = User.builder() - .name("발신자 겸 판매자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); + .name("발신자 겸 판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); final User 수신자_겸_기존_입찰자 = User.builder() - .name("수신자 겸 기존 입찰자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12347") - .build(); + .name("수신자 겸 기존 입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); final User 새로운_입찰자 = User.builder() - .name("새로운 입찰자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("13579") - .build(); + .name("새로운 입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("13579") + .build(); final User 질문자 = User.builder() - .name("질문자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12038") - .build(); + .name("질문자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12038") + .build(); userRepository.save(발신자_겸_판매자); userRepository.save(수신자_겸_기존_입찰자); userRepository.save(새로운_입찰자); userRepository.save(질문자); final Auction 경매 = Auction.builder() - .seller(발신자_겸_판매자) - .title("경매글") - .description("경매글 설명") - .bidUnit(new BidUnit(100)) - .startPrice(new Price(100)) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); + .seller(발신자_겸_판매자) + .title("경매글") + .description("경매글 설명") + .bidUnit(new BidUnit(100)) + .startPrice(new Price(100)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); auctionRepository.save(경매); final AuctionImage 경매_이미지 = new AuctionImage("upload.jpg", "store.jpg"); @@ -121,6 +121,7 @@ void setUpFixture() { "baseUrl", 이미지_절대_경로 )); 메시지_전송_데이터 = Map.of( + "type", "message", "chatRoomId", String.valueOf(채팅방.getId()), "receiverId", String.valueOf(수신자_겸_기존_입찰자.getId()), "contents", "메시지 내용" @@ -129,11 +130,11 @@ void setUpFixture() { final Message 저장된_메시지 = messageRepository.save( Message.builder() - .chatRoom(채팅방) - .writer(발신자_겸_판매자) - .receiver(수신자_겸_기존_입찰자) - .contents("메시지 내용") - .build() + .chatRoom(채팅방) + .writer(발신자_겸_판매자) + .receiver(수신자_겸_기존_입찰자) + .contents("메시지 내용") + .build() ); final AuctionAndImageDto auctionAndImageDto = new AuctionAndImageDto(경매, 경매_이미지); final BidDto 입찰_DTO = new BidDto(수신자_겸_기존_입찰자.getId(), auctionAndImageDto, 이미지_절대_경로); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/dto/ChattingTypeTest.java b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/dto/ChattingTypeTest.java new file mode 100644 index 000000000..52604aa02 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/dto/ChattingTypeTest.java @@ -0,0 +1,33 @@ +package com.ddang.ddang.websocket.handler.dto; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ChattingTypeTest { + + @Test + void 타입에_해당하는_enum을_반환한다() { + // given + final Map data = Map.of("type", "message"); + + // when + final ChattingType actual = ChattingType.findValue(data.get("type")); + + // then + assertThat(actual).isEqualTo(ChattingType.MESSAGE); + } + + + @Test + void 해당하는_타입이_없는_경우_예외를_던진다() { + // given + final Map data = Map.of("type", "wrong type"); + + // when & then + assertThatThrownBy(() -> ChattingType.findValue(data.get("type"))).isInstanceOf(IllegalArgumentException.class); + } +}