Skip to content
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,6 +25,7 @@
@RequestMapping("/api")
public class ChatRoomController {
private final ChatRoomService chatRoomService;
private final ChatMessageService chatMessageService;

@GetMapping("/v1/users/chat-rooms")
public ResponseEntity<List<UserChatRoomsResponse>> getUserChatRooms(
Expand All @@ -50,6 +52,7 @@ public ResponseEntity<Slice<ChatMessageResponse>> getChatMessages(
Pageable pageable
) {
Long userId = userDetails.getUserId();
chatMessageService.markMessagesAsRead(chatRoomId, userId);
Slice<ChatMessageResponse> messages = chatRoomService
.findChatMessagesByChatRoomId(userId, chatRoomId, pageable);
return ResponseEntity.ok(messages);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatMessage> lastMessage = findLastMessage(chatRoom, chatMessageRepository);
return new UserChatRoomsResponse(
chatRoom.getChatRoomId(),
Expand All @@ -30,23 +36,32 @@ 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<ChatMessage> lastMessage = findLastMessage(chatRoom, chatMessageRepository);

long unreadCount = chatMessageRepository.countByChatRoomIdAndSenderIdNotAndIsReadFalse(
chatRoom.getChatRoomId(),
userId
);

return new CoachChatRoomsResponse(
chatRoom.getChatRoomId(),
user.getUserId(),
user.getNickname(),
user.getProfileImageUrl(),
chatRoom.getMatching() != null && chatRoom.getMatching().getIsMatching(),
lastMessage.map(ChatMessage::getMessage).orElse(""),
unreadCount,
lastMessage.map(ChatMessage::getCreatedAt).orElse(null)
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public record CoachChatRoomsResponse(
String userProfileImageUrl,
boolean isMatching,
String lastMessage,
long unreadCount,
LocalDateTime lastMessageCreatedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public record UserChatRoomsResponse(
List<String> coachingSports,
String activeHours,
String lastMessage,
long unreadCount,
LocalDateTime lastMessageCreatedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@

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;

public interface ChatMessageRepository extends MongoRepository<ChatMessage, String> {
Optional<ChatMessage> findTopByChatRoomIdOrderByCreatedAt(Long chatRoomId);

Slice<ChatMessage> 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);
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ public Long createChatRoom(ChatRoomRequest chatRoomRequest) {

@Transactional(readOnly = true)
public List<UserChatRoomsResponse> 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());
}

Expand All @@ -75,24 +76,25 @@ public List<CoachChatRoomsResponse> 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<ChatMessageResponse> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Expand Down