diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatMessageController.java b/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatMessageController.java new file mode 100644 index 00000000..e390fa29 --- /dev/null +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatMessageController.java @@ -0,0 +1,29 @@ +package site.coach_coach.coach_coach_server.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.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; + +import lombok.RequiredArgsConstructor; +import site.coach_coach.coach_coach_server.auth.userdetails.CustomUserDetails; +import site.coach_coach.coach_coach_server.chat.dto.request.ChatMessageRequest; +import site.coach_coach.coach_coach_server.chat.service.ChatMessageService; + +@Controller +@RequiredArgsConstructor +public class ChatMessageController { + + private final ChatMessageService chatMessageService; + + @MessageMapping("/chat-rooms/{chatRoomId}") + public void sendMessage( + @DestinationVariable("chatRoomId") Long chatRoomId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @Payload ChatMessageRequest messageRequest + ) { + Long senderId = userDetails.getUserId(); + chatMessageService.addMessage(chatRoomId, senderId, messageRequest); + } +} diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatRoomController.java b/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatRoomController.java index 26de2741..41542243 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatRoomController.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/controller/ChatRoomController.java @@ -16,6 +16,7 @@ import site.coach_coach.coach_coach_server.chat.dto.response.ChatMessageResponse; import site.coach_coach.coach_coach_server.chat.dto.response.CoachChatRoomsResponse; import site.coach_coach.coach_coach_server.chat.dto.response.UserChatRoomsResponse; +import site.coach_coach.coach_coach_server.chat.service.ChatMessageService; import site.coach_coach.coach_coach_server.chat.service.ChatRoomService; import site.coach_coach.coach_coach_server.user.domain.User; @@ -24,6 +25,7 @@ @RequestMapping("/api") public class ChatRoomController { private final ChatRoomService chatRoomService; + private final ChatMessageService chatMessageService; @GetMapping("/v1/users/chat-rooms") public ResponseEntity> getUserChatRooms( @@ -50,6 +52,7 @@ public ResponseEntity> getChatMessages( Pageable pageable ) { Long userId = userDetails.getUserId(); + chatMessageService.markMessagesAsRead(chatRoomId, userId); Slice messages = chatRoomService .findChatMessagesByChatRoomId(userId, chatRoomId, pageable); return ResponseEntity.ok(messages); diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/domain/ChatMessage.java b/src/main/java/site/coach_coach/coach_coach_server/chat/domain/ChatMessage.java index 8817a700..2a42814d 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/domain/ChatMessage.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/domain/ChatMessage.java @@ -7,14 +7,12 @@ import org.springframework.data.mongodb.core.mapping.Document; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import site.coach_coach.coach_coach_server.common.domain.RoleEnum; @Document(collection = "chat_messages") @Getter -@Builder @NoArgsConstructor @AllArgsConstructor public class ChatMessage { @@ -24,7 +22,30 @@ public class ChatMessage { private Long senderId; private RoleEnum senderRole; private String message; - private boolean isRead = false; + private boolean isRead; @CreationTimestamp private LocalDateTime createdAt; + + private ChatMessage( + Long chatRoomId, + Long senderId, + RoleEnum senderRole, + String message + ) { + this.chatRoomId = chatRoomId; + this.senderId = senderId; + this.senderRole = senderRole; + this.message = message; + this.isRead = false; + this.createdAt = LocalDateTime.now(); + } + + public static ChatMessage of( + Long chatRoomId, + Long senderId, + RoleEnum senderRole, + String message + ) { + return new ChatMessage(chatRoomId, senderId, senderRole, message); + } } diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/mapper/ChatRoomMapper.java b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/mapper/ChatRoomMapper.java index 156b983a..01f33bab 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/mapper/ChatRoomMapper.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/mapper/ChatRoomMapper.java @@ -16,10 +16,16 @@ public class ChatRoomMapper { public static UserChatRoomsResponse toUserChatRoomsResponse( ChatRoom chatRoom, + Long userId, ChatMessageRepository chatMessageRepository ) { Coach coach = chatRoom.getCoach(); User coachUser = coach.getUser(); + + long unreadCount = chatMessageRepository.countByChatRoomIdAndSenderIdNotAndIsReadFalse( + chatRoom.getChatRoomId(), + userId + ); Optional lastMessage = findLastMessage(chatRoom, chatMessageRepository); return new UserChatRoomsResponse( chatRoom.getChatRoomId(), @@ -30,16 +36,24 @@ public static UserChatRoomsResponse toUserChatRoomsResponse( getCoachingSports(coach), coach.getActiveHours(), lastMessage.map(ChatMessage::getMessage).orElse(""), + unreadCount, lastMessage.map(ChatMessage::getCreatedAt).orElse(null) ); } public static CoachChatRoomsResponse toCoachChatRoomsResponse( ChatRoom chatRoom, + Long userId, ChatMessageRepository chatMessageRepository ) { User user = chatRoom.getUser(); Optional lastMessage = findLastMessage(chatRoom, chatMessageRepository); + + long unreadCount = chatMessageRepository.countByChatRoomIdAndSenderIdNotAndIsReadFalse( + chatRoom.getChatRoomId(), + userId + ); + return new CoachChatRoomsResponse( chatRoom.getChatRoomId(), user.getUserId(), @@ -47,6 +61,7 @@ public static CoachChatRoomsResponse toCoachChatRoomsResponse( user.getProfileImageUrl(), chatRoom.getMatching() != null && chatRoom.getMatching().getIsMatching(), lastMessage.map(ChatMessage::getMessage).orElse(""), + unreadCount, lastMessage.map(ChatMessage::getCreatedAt).orElse(null) ); } diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/request/ChatMessageRequest.java b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/request/ChatMessageRequest.java index ed9ce5cb..48ea8f6d 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/request/ChatMessageRequest.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/request/ChatMessageRequest.java @@ -1,19 +1,8 @@ package site.coach_coach.coach_coach_server.chat.dto.request; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import site.coach_coach.coach_coach_server.common.domain.RoleEnum; public record ChatMessageRequest( - @NotNull - Long chatRoomId, - - @NotNull - Long senderId, - - @NotBlank - RoleEnum senderRole, - @NotBlank String message ) { diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/CoachChatRoomsResponse.java b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/CoachChatRoomsResponse.java index 0813ad07..568dc229 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/CoachChatRoomsResponse.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/CoachChatRoomsResponse.java @@ -9,6 +9,7 @@ public record CoachChatRoomsResponse( String userProfileImageUrl, boolean isMatching, String lastMessage, + long unreadCount, LocalDateTime lastMessageCreatedAt ) { } diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/UserChatRoomsResponse.java b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/UserChatRoomsResponse.java index e9edeab4..a20826e6 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/UserChatRoomsResponse.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/dto/response/UserChatRoomsResponse.java @@ -12,6 +12,7 @@ public record UserChatRoomsResponse( List coachingSports, String activeHours, String lastMessage, + long unreadCount, LocalDateTime lastMessageCreatedAt ) { } diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/repository/ChatMessageRepository.java b/src/main/java/site/coach_coach/coach_coach_server/chat/repository/ChatMessageRepository.java index 27491ca4..87188999 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/repository/ChatMessageRepository.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/repository/ChatMessageRepository.java @@ -4,7 +4,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; import site.coach_coach.coach_coach_server.chat.domain.ChatMessage; @@ -12,4 +14,10 @@ public interface ChatMessageRepository extends MongoRepository findTopByChatRoomIdOrderByCreatedAt(Long chatRoomId); Slice findByChatRoomIdOrderByCreatedAtDesc(Long chatRoomId, Pageable pageable); + + long countByChatRoomIdAndSenderIdNotAndIsReadFalse(Long chatRoomId, Long senderId); + + @Modifying + @Query("{'chatRoomId': ?0, 'senderId': {$ne: ?1}, 'isRead': false}") + void markMessagesAsRead(Long chatRoomId, Long senderId); } diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatMessageService.java b/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatMessageService.java new file mode 100644 index 00000000..081ed271 --- /dev/null +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatMessageService.java @@ -0,0 +1,70 @@ +package site.coach_coach.coach_coach_server.chat.service; + +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import site.coach_coach.coach_coach_server.chat.domain.ChatMessage; +import site.coach_coach.coach_coach_server.chat.domain.ChatRoom; +import site.coach_coach.coach_coach_server.chat.dto.request.ChatMessageRequest; +import site.coach_coach.coach_coach_server.chat.repository.ChatMessageRepository; +import site.coach_coach.coach_coach_server.chat.repository.ChatRoomRepository; +import site.coach_coach.coach_coach_server.common.constants.ErrorMessage; +import site.coach_coach.coach_coach_server.common.domain.RoleEnum; +import site.coach_coach.coach_coach_server.common.exception.AccessDeniedException; +import site.coach_coach.coach_coach_server.common.exception.NotFoundException; + +@Service +@RequiredArgsConstructor +public class ChatMessageService { + private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + private final SimpMessageSendingOperations messagingTemplate; + private final ChatRoomService chatRoomService; + + @Transactional + public void addMessage(Long chatRoomId, Long senderId, ChatMessageRequest messageRequest) { + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_CHAT_ROOM)); + + chatRoomService.validateUserRoleForChatRoom(senderId, chatRoom); + RoleEnum senderRole = determineSenderRole(chatRoom, senderId); + + messagingTemplate.convertAndSend("/sub/chat-rooms/" + chatRoomId, + ChatMessage.of(chatRoomId, senderId, senderRole, messageRequest.message())); + } + + private RoleEnum determineSenderRole(ChatRoom chatRoom, Long senderId) { + if (chatRoom.getUser().getUserId().equals(senderId)) { + return RoleEnum.USER; + } else if (chatRoom.getCoach().getUser().getUserId().equals(senderId)) { + return RoleEnum.COACH; + } else { + throw new AccessDeniedException(); + } + } + + @Transactional + public void markMessagesAsRead(Long chatRoomId, Long userId) { + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_CHAT_ROOM)); + chatRoomService.validateUserRoleForChatRoom(userId, chatRoom); + RoleEnum senderRole = determineSenderRole(chatRoom, userId); + Long receiverId = determineReceiverId(chatRoom, userId, senderRole); + chatMessageRepository.markMessagesAsRead(chatRoomId, receiverId); + } + + private Long determineReceiverId(ChatRoom chatRoom, Long senderId, RoleEnum role) { + if (role == RoleEnum.USER) { + if (chatRoom.getUser().getUserId().equals(senderId)) { + return chatRoom.getCoach().getUser().getUserId(); + } + } else { + if (chatRoom.getCoach().getUser().getUserId().equals(senderId)) { + return chatRoom.getUser().getUserId(); + } + } + throw new AccessDeniedException(); + } +} diff --git a/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatRoomService.java b/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatRoomService.java index 163537f8..bdf5c817 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatRoomService.java +++ b/src/main/java/site/coach_coach/coach_coach_server/chat/service/ChatRoomService.java @@ -60,9 +60,10 @@ public Long createChatRoom(ChatRoomRequest chatRoomRequest) { @Transactional(readOnly = true) public List findChatRoomsForUser(User user) { + Long userId = user.getUserId(); return chatRoomRepository.findByUser(user) .stream() - .map(chatRoom -> ChatRoomMapper.toUserChatRoomsResponse(chatRoom, chatMessageRepository)) + .map(chatRoom -> ChatRoomMapper.toUserChatRoomsResponse(chatRoom, userId, chatMessageRepository)) .collect(Collectors.toList()); } @@ -75,24 +76,25 @@ public List findChatRoomsForCoach(User user) { Coach coach = coachRepository.findByUser(user) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COACH)); + Long userId = user.getUserId(); return chatRoomRepository.findByCoach_CoachId(coach.getCoachId()) .stream() - .map(chatRoom -> ChatRoomMapper.toCoachChatRoomsResponse(chatRoom, chatMessageRepository)) + .map(chatRoom -> ChatRoomMapper.toCoachChatRoomsResponse(chatRoom, userId, chatMessageRepository)) .collect(Collectors.toList()); } @Transactional(readOnly = true) public Slice findChatMessagesByChatRoomId(Long userId, Long chatRoomId, Pageable pageable) { - validateUserRoleForChatRoom(userId, chatRoomId); + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_CHAT_ROOM)); + validateUserRoleForChatRoom(userId, chatRoom); return chatMessageRepository .findByChatRoomIdOrderByCreatedAtDesc(chatRoomId, pageable) .map(ChatMessageMapper::toChatMessageResponse); } - private void validateUserRoleForChatRoom(Long userId, Long chatRoomId) { - ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_CHAT_ROOM)); - + @Transactional(readOnly = true) + public void validateUserRoleForChatRoom(Long userId, ChatRoom chatRoom) { boolean isUser = chatRoom.getUser().getUserId().equals(userId); boolean isCoach = chatRoom.getCoach() != null && chatRoom.getCoach().getUser().getUserId().equals(userId); diff --git a/src/main/java/site/coach_coach/coach_coach_server/config/SecurityConfig.java b/src/main/java/site/coach_coach/coach_coach_server/config/SecurityConfig.java index 9e4c669e..467d74a8 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/config/SecurityConfig.java +++ b/src/main/java/site/coach_coach/coach_coach_server/config/SecurityConfig.java @@ -50,7 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti authorizeRequests .requestMatchers("/api/v1/auth/login", "/api/v1/auth/signup", "/api/v1/test", "/api/v1/auth/check-email", "/api/v1/auth/check-nickname", "/api/v1/auth/reissue", - "/api/v1/auth", "/oauth2/", "/login/oauth2/").permitAll() + "/api/v1/auth", "/oauth2/", "/login/oauth2/", "/ws/**").permitAll() .anyRequest() .authenticated() )