From 3e69cbf205c5b967ffb43327a2bfe6c02d6d9356 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Sat, 28 Jun 2025 00:16:30 +0900 Subject: [PATCH 01/15] =?UTF-8?q?refactor=20:=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20Chat=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20ChatRoom?= =?UTF-8?q?=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/stomp/StompMessageService.java | 108 ------------- .../chat/chatroom/entity/ParticipantInfo.java | 70 -------- .../chat/chatroom/entity/Participants.java | 48 ------ .../ChatRoomCreateFailException.java | 7 - .../exception/ChatRoomExistedException.java | 17 -- .../exception/ChatRoomNotFoundException.java | 7 - .../service/ChatRoomEventListener.java | 26 --- .../chatroom/service/ChatRoomService.java | 152 ------------------ .../controller/ChatRoomController.java | 12 +- .../entity => domain/chatroom}/ChatRoom.java | 19 ++- .../chat/domain/chatroom/ParticipantInfo.java | 61 +++++++ .../chat/domain/chatroom/Participants.java | 97 +++++++++++ .../event/ChatRoomNotificationEvent.java | 4 +- .../request}/ChatRoomCreateRequestDto.java | 15 +- .../response/ChatRoomCreateResponseDto.java | 15 ++ .../response}/ChatRoomListResponseDto.java | 6 +- .../chat/event/ChatRoomEventListener.java | 29 ++++ .../exception/ChatRoomExistedException.java | 15 ++ .../repository/ChatRoomRepository.java | 7 +- .../domain/chat/service/ChatRoomService.java | 97 +++++++++++ .../chat/service/ChatRoomValidator.java | 70 ++++++++ 21 files changed, 422 insertions(+), 460 deletions(-) delete mode 100644 src/main/java/inu/codin/codin/common/stomp/StompMessageService.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ParticipantInfo.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/entity/Participants.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomCreateFailException.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomExistedException.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomNotFoundException.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomEventListener.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomService.java rename src/main/java/inu/codin/codin/domain/chat/{chatroom => }/controller/ChatRoomController.java (82%) rename src/main/java/inu/codin/codin/domain/chat/{chatroom/entity => domain/chatroom}/ChatRoom.java (76%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java rename src/main/java/inu/codin/codin/domain/chat/{chatroom/dto => domain/chatroom}/event/ChatRoomNotificationEvent.java (82%) rename src/main/java/inu/codin/codin/domain/chat/{chatroom/dto => dto/chatroom/request}/ChatRoomCreateRequestDto.java (64%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomCreateResponseDto.java rename src/main/java/inu/codin/codin/domain/chat/{chatroom/dto => dto/chatroom/response}/ChatRoomListResponseDto.java (93%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/event/ChatRoomEventListener.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/exception/ChatRoomExistedException.java rename src/main/java/inu/codin/codin/domain/chat/{chatroom => }/repository/ChatRoomRepository.java (77%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/service/ChatRoomService.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java diff --git a/src/main/java/inu/codin/codin/common/stomp/StompMessageService.java b/src/main/java/inu/codin/codin/common/stomp/StompMessageService.java deleted file mode 100644 index 410682f1..00000000 --- a/src/main/java/inu/codin/codin/common/stomp/StompMessageService.java +++ /dev/null @@ -1,108 +0,0 @@ -package inu.codin.codin.common.stomp; - -import inu.codin.codin.common.exception.NotFoundException; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; -import inu.codin.codin.domain.chat.chatroom.repository.ChatRoomRepository; -import inu.codin.codin.domain.chat.chatting.dto.event.UpdateUnreadCountEvent; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; -import inu.codin.codin.domain.chat.chatting.repository.ChattingRepository; -import inu.codin.codin.domain.user.entity.UserEntity; -import inu.codin.codin.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.bson.types.ObjectId; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -@Service -@RequiredArgsConstructor -@Slf4j -public class StompMessageService { - - private final Map sessionStore = new ConcurrentHashMap<>(); - - private final ChatRoomRepository chatRoomRepository; - private final UserRepository userRepository; - private final ChattingRepository chattingRepository; - private final ApplicationEventPublisher eventPublisher; - - public void connectSession(StompHeaderAccessor headerAccessor) { - String sessionId = headerAccessor.getSessionId(); - sessionStore.put(sessionId, ""); - log.info("[STOMP CONNECT] session 연결 : {}", sessionId); - } - - public void enterToChatRoom(StompHeaderAccessor headerAccessor){ - Result result = getResult(headerAccessor); - sessionStore.put(headerAccessor.getSessionId(), result.chatroom().get_id().toString()); - log.info("[STOMP SUBSCRIBE] session : {}, chatRoomId : {} ", headerAccessor.getSessionId(), result.chatroom().get_id().toString()); - - List chattings = updateUnreadCount(result.chatroom.get_id(), result.user.get_id()); - result.chatroom.getParticipants().enter(result.user.get_id()); - chatRoomRepository.save(result.chatroom); - if (!chattings.isEmpty()) - eventPublisher.publishEvent(new UpdateUnreadCountEvent(this, chattings, result.chatroom.get_id().toString())); - } - - public void exitToChatRoom(StompHeaderAccessor headerAccessor) { - Result result = getResult(headerAccessor); - if (result == null) throw new NotFoundException("[exitToChatRoom] UNSUBSCRIBE 실패"); - result.chatroom().getParticipants().exit(result.user().get_id()); - chatRoomRepository.save(result.chatroom()); - log.info("[STOMP UNSUBSCRIBE] session : {}, chatRoomId : {} ", headerAccessor.getSessionId(), result.chatroom().get_id().toString()); - } - - public void disconnectSession(StompHeaderAccessor headerAccessor){ - sessionStore.remove(headerAccessor.getSessionId()); - log.info("[STOMP DISCONNECT] session : {} ", headerAccessor.getSessionId()); - - } - - private Result getResult(StompHeaderAccessor headerAccessor) { - String email = null; - if (headerAccessor.getUser() != null) email = headerAccessor.getUser().getName(); - else log.error("헤더에서 유저를 찾을 수 없습니다. command : {}, sessionId : {}", headerAccessor.getCommand(), headerAccessor.getSessionId()); - - String chatroomId = headerAccessor.getFirstNativeHeader("chatRoomId"); - if (chatroomId == null || !ObjectId.isValid(chatroomId)) { - log.error("chatRoomId을 찾을 수 없습니다. command : {}, sessionId : {}, chatRoomId : {}", - headerAccessor.getCommand(), headerAccessor.getSessionId(), chatroomId); - return null; - } else { - Optional chatroom = chatRoomRepository.findById(new ObjectId(chatroomId)); - if (chatroom.isEmpty()) log.error("채팅방을 찾을 수 없습니다. command : {}, sessionId : {}, chatroomId : {}", - headerAccessor.getCommand(), headerAccessor.getSessionId(), chatroomId); - Optional user = userRepository.findByEmailAndStatusAll(email); - if (user.isEmpty()) log.error("유저를 찾을 수 없습니다. command : {}, sessionId : {}, email : {}", - headerAccessor.getCommand(), headerAccessor.getSessionId(), email); - if (chatroom.isPresent() && user.isPresent()) - return new Result(chatroom.get(), user.get()); - } - return null; - } - - - - private record Result(ChatRoom chatroom, UserEntity user) { - } - - private List updateUnreadCount(ObjectId chatRoomId, ObjectId userId){ - ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(()-> new NotFoundException("채팅방을 찾을 수 없습니다.")); - List chattings = chattingRepository.findAllByChatRoomIdOrderByCreatedAtDesc(chatRoomId) - .stream() - .limit(chatRoom.getParticipants().getInfo().get(userId).getUnreadMessage()) - .map(chatting -> { - chatting.minusUnread(); - return chattingRepository.save(chatting); - }).toList(); - return chattings; - - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ParticipantInfo.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ParticipantInfo.java deleted file mode 100644 index ce4dbea3..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ParticipantInfo.java +++ /dev/null @@ -1,70 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.entity; - -import inu.codin.codin.common.dto.BaseTimeEntity; -import lombok.*; -import org.bson.types.ObjectId; - -import java.time.LocalDateTime; - -@Getter -@NoArgsConstructor -public class ParticipantInfo extends BaseTimeEntity { - - private ObjectId userId; - private boolean isConnected = false; - private int unreadMessage = 0; - - private boolean isLeaved = false; - private LocalDateTime whenLeaved; - private boolean notificationsEnabled = true; - - @Builder - public ParticipantInfo(ObjectId userId, boolean isConnected, int unreadMessage, boolean notificationsEnabled, boolean isLeaved, LocalDateTime whenLeaved) { - this.userId = userId; - this.isConnected = isConnected; - this.unreadMessage = unreadMessage; - this.notificationsEnabled = notificationsEnabled; - this.isLeaved = isLeaved; - this.whenLeaved = whenLeaved; - } - - public void updateNotification() { - this.notificationsEnabled = !notificationsEnabled; - } - - public static ParticipantInfo enter(ObjectId userId){ - return ParticipantInfo.builder() - .userId(userId) - .isConnected(false) - .unreadMessage(0) - .isLeaved(false) - .whenLeaved(null) - .notificationsEnabled(true) - .build(); - } - - public void plusUnread(){ - this.unreadMessage++; - } - - public void connect(){ - this.isConnected = true; - this.unreadMessage = 0; - } - - public void disconnect(){ - this.isConnected = false; - this.unreadMessage = 0; - setUpdatedAt(); - } - - public void leave(){ - this.isLeaved = true; - this.whenLeaved = LocalDateTime.now(); - } - - public void remain(){ - this.isLeaved = false; - } - -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/Participants.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/Participants.java deleted file mode 100644 index 9f5c5e17..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/Participants.java +++ /dev/null @@ -1,48 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.entity; - -import lombok.Getter; -import lombok.Setter; -import org.bson.types.ObjectId; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Getter -@Setter -public class Participants { - - private Map info = new ConcurrentHashMap<>(); - - public void create(ObjectId memberId){ - info.put(memberId, ParticipantInfo.enter(memberId)); - } - - public boolean getMessage(ObjectId receiver) { - ParticipantInfo member; - if((member=info.get(receiver))==null) { - return false; - } - member.plusUnread(); - return true; - } - - public void enter(ObjectId memberId){ - ParticipantInfo participantInfo; - if ((participantInfo = info.get(memberId))==null) { - return; - } - - participantInfo.connect(); - info.put(memberId, participantInfo); - } - - public void exit(ObjectId memberId) { - ParticipantInfo participantInfo; - if ((participantInfo = info.get(memberId))==null) { - return; - } - - participantInfo.disconnect(); - info.put(memberId, participantInfo); - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomCreateFailException.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomCreateFailException.java deleted file mode 100644 index 06ed9a88..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomCreateFailException.java +++ /dev/null @@ -1,7 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.exception; - -public class ChatRoomCreateFailException extends RuntimeException{ - public ChatRoomCreateFailException(String message) { - super(message); - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomExistedException.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomExistedException.java deleted file mode 100644 index 99617999..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomExistedException.java +++ /dev/null @@ -1,17 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.exception; - -import lombok.Getter; -import org.bson.types.ObjectId; - -@Getter -public class ChatRoomExistedException extends RuntimeException{ - - private final int errorCode; - private final ObjectId chatRoomId; - - public ChatRoomExistedException(String msg, int errorCode, ObjectId chatRoomId){ - super(msg); - this.errorCode = errorCode; - this.chatRoomId = chatRoomId; - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomNotFoundException.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomNotFoundException.java deleted file mode 100644 index 7ac7c3cd..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/exception/ChatRoomNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.exception; - -public class ChatRoomNotFoundException extends RuntimeException { - public ChatRoomNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomEventListener.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomEventListener.java deleted file mode 100644 index 8bbb5b8e..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomEventListener.java +++ /dev/null @@ -1,26 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.service; - -import inu.codin.codin.domain.chat.chatroom.dto.event.ChatRoomNotificationEvent; -import inu.codin.codin.domain.chat.chatroom.entity.ParticipantInfo; -import inu.codin.codin.domain.notification.service.NotificationService; -import lombok.RequiredArgsConstructor; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ChatRoomEventListener { - - private final NotificationService notificationService; - - @Async - @EventListener - public void handleChatRoomNotification(ChatRoomNotificationEvent event){ - if (event.getParticipants().getInfo().containsKey(event.getReceiverId())){ - ParticipantInfo participant = event.getParticipants().getInfo().get(event.getReceiverId()); - if (participant.isNotificationsEnabled()) - notificationService.sendNotificationMessageByChat(participant.getUserId(), event.getChatRoomId()); - } - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomService.java b/src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomService.java deleted file mode 100644 index 25150c0a..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/service/ChatRoomService.java +++ /dev/null @@ -1,152 +0,0 @@ -package inu.codin.codin.domain.chat.chatroom.service; - -import inu.codin.codin.common.exception.NotFoundException; -import inu.codin.codin.common.security.util.SecurityUtils; -import inu.codin.codin.domain.block.service.BlockService; -import inu.codin.codin.domain.chat.chatroom.dto.ChatRoomCreateRequestDto; -import inu.codin.codin.domain.chat.chatroom.dto.ChatRoomListResponseDto; -import inu.codin.codin.domain.chat.chatroom.dto.event.ChatRoomNotificationEvent; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; -import inu.codin.codin.domain.chat.chatroom.entity.ParticipantInfo; -import inu.codin.codin.domain.chat.chatroom.exception.ChatRoomCreateFailException; -import inu.codin.codin.domain.chat.chatroom.exception.ChatRoomExistedException; -import inu.codin.codin.domain.chat.chatroom.exception.ChatRoomNotFoundException; -import inu.codin.codin.domain.chat.chatroom.repository.ChatRoomRepository; -import inu.codin.codin.domain.notification.service.NotificationService; -import inu.codin.codin.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.bson.types.ObjectId; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Service; - -import java.util.*; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ChatRoomService { - - private final ChatRoomRepository chatRoomRepository; - private final UserRepository userRepository; - - private final BlockService blockService; - private final ApplicationEventPublisher eventPublisher; - - - public Map createChatRoom(ChatRoomCreateRequestDto chatRoomCreateRequestDto) { - ObjectId senderId = SecurityUtils.getCurrentUserId(); - isValidated(chatRoomCreateRequestDto, senderId); //유효성 검사 - - log.info("[채팅방 생성 요청] 송신자 ID: {}, 수신자 ID: {}", senderId, chatRoomCreateRequestDto.getReceiverId()); - userRepository.findById(new ObjectId(chatRoomCreateRequestDto.getReceiverId())) - .orElseThrow(() -> { - log.error("[Receive 유저 확인 실패] 수신자 ID: {}를 찾을 수 없습니다.", chatRoomCreateRequestDto.getReceiverId()); - return new NotFoundException("Receive 유저를 찾을 수 없습니다."); - }); - log.info("[Receive 유저 확인 완료] 수신자 ID: {}", chatRoomCreateRequestDto.getReceiverId()); - - ChatRoom chatRoom = ChatRoom.of(chatRoomCreateRequestDto, senderId); - chatRoomRepository.save(chatRoom); - log.info("[채팅방 생성 완료] 채팅방 ID: {}, 송신자 ID: {}, 수신자 ID: {}", chatRoom.get_id(), senderId, chatRoomCreateRequestDto.getReceiverId()); - - eventPublisher.publishEvent(new ChatRoomNotificationEvent(this, - chatRoom.get_id(), new ObjectId(chatRoomCreateRequestDto.getReceiverId()), chatRoom.getParticipants())); - - Map response = new HashMap<>(); - response.put("chatRoomId", chatRoom.get_id().toString()); - log.info("[채팅방 생성 응답] 생성된 채팅방 ID: {}", chatRoom.get_id()); - return response; - } - - private void isValidated(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { - if (senderId.toString().equals(chatRoomCreateRequestDto.getReceiverId())){ - throw new ChatRoomCreateFailException("자기 자신과는 채팅방을 생성할 수 없습니다."); - } - - Optional existedChatroom = chatRoomRepository.findByReferenceIdAndParticipantsContaining(new ObjectId(chatRoomCreateRequestDto.getReferenceId()), - senderId, new ObjectId(chatRoomCreateRequestDto.getReceiverId())); - if (existedChatroom.isPresent()){ - ParticipantInfo participantInfo= existedChatroom.get().getParticipants().getInfo().get(senderId); - if (participantInfo.isLeaved()){ - participantInfo.remain(); - chatRoomRepository.save(existedChatroom.get()); - } - throw new ChatRoomExistedException("해당 reference에서 시작된 채팅방이 존재합니다.", 403, existedChatroom.get().get_id()); - } - } - - public List getAllChatRoomByUser() { - ObjectId userId = SecurityUtils.getCurrentUserId(); - log.info("[유저의 채팅방 조회] 유저 ID: {}", userId); - // 차단 목록 조회 - List blockedUsersId = blockService.getBlockedUsers(); - - List chatRooms = chatRoomRepository.findByParticipantIsNotLeavedAndDeletedIsNull(userId); - log.info("[채팅방 조회 결과] 유저 ID: {}가 참여 중인 채팅방 개수: {}", userId, chatRooms.size()); - return chatRooms.stream() - .filter(chatRoom -> chatRoom.getParticipants().getInfo().keySet().stream() - .noneMatch(blockedUsersId::contains)) - .sorted(Comparator.comparing(ChatRoom::getCurrentMessageDate,Comparator.nullsLast(Comparator.reverseOrder()))) - .map(chatRoom -> ChatRoomListResponseDto.of(chatRoom, userId)).toList(); - - } - - public void leaveChatRoom(String chatRoomId) { - ObjectId userId = SecurityUtils.getCurrentUserId(); - log.info("[채팅방 나가기 요청] 유저 ID: {}, 채팅방 ID: {}", userId, chatRoomId); - - ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> { - log.error("[채팅방 확인 실패] 채팅방 ID: {}를 찾을 수 없습니다.", chatRoomId); - return new ChatRoomNotFoundException("채팅방을 찾을 수 없습니다."); - }); - - Map info = chatRoom.getParticipants().getInfo(); - if (info.containsKey(userId)){ - info.get(userId).leave(); - info.get(userId).disconnect(); - } else { - log.warn("[채팅방 탈퇴 실패] 유저 ID: {}는 채팅방에 참여하지 않았습니다.", userId); - throw new ChatRoomNotFoundException("회원이 포함된 채팅방을 찾을 수 없습니다."); - } - - boolean isAllLeaved = chatRoom.getParticipants().getInfo().values() - .stream() - .allMatch(ParticipantInfo::isLeaved); // 모든 참가자가 떠났는지 확인 - - if (isAllLeaved){ - chatRoom.delete(); - log.info("[채팅방 삭제] 채팅방 ID: {}에 더 이상 참여자가 없어 채팅방을 삭제합니다.", chatRoomId); - } - chatRoomRepository.save(chatRoom); - } - - public void setNotificationChatRoom(String chatRoomId) { - ObjectId userId = SecurityUtils.getCurrentUserId(); - log.info("[알림 설정 요청] 유저 ID: {}, 채팅방 ID: {}", userId, chatRoomId); - - ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> { - log.error("[알림 설정 실패] 채팅방을 찾을 수 없습니다. 채팅방 ID: {}", chatRoomId); - return new ChatRoomNotFoundException("채팅방을 찾을 수 없습니다."); - }); - - Map info = chatRoom.getParticipants().getInfo(); - log.info("[채팅방 확인] 채팅방 ID: {}, 참여자 수: {}", chatRoomId, info.size()); - - info.get(userId).updateNotification(); - chatRoomRepository.save(chatRoom); - log.info("[알림 설정 완료] 채팅방 ID: {}에 알림 설정 완료", chatRoomId); - } - - public Integer countOfParticipating(ObjectId chatRoomId){ - ChatRoom chatroom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new NotFoundException("채팅방을 찾을 수 없습니다.")); - int count = 0; - for (ParticipantInfo info : chatroom.getParticipants().getInfo().values()){ - if (info.isConnected()) count++; - } - return count; - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/controller/ChatRoomController.java b/src/main/java/inu/codin/codin/domain/chat/controller/ChatRoomController.java similarity index 82% rename from src/main/java/inu/codin/codin/domain/chat/chatroom/controller/ChatRoomController.java rename to src/main/java/inu/codin/codin/domain/chat/controller/ChatRoomController.java index 73344597..b3c1f71e 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/controller/ChatRoomController.java +++ b/src/main/java/inu/codin/codin/domain/chat/controller/ChatRoomController.java @@ -1,10 +1,10 @@ -package inu.codin.codin.domain.chat.chatroom.controller; +package inu.codin.codin.domain.chat.controller; import inu.codin.codin.common.response.ListResponse; import inu.codin.codin.common.response.SingleResponse; -import inu.codin.codin.domain.chat.chatroom.dto.ChatRoomCreateRequestDto; -import inu.codin.codin.domain.chat.chatroom.dto.ChatRoomListResponseDto; -import inu.codin.codin.domain.chat.chatroom.service.ChatRoomService; +import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; +import inu.codin.codin.domain.chat.dto.chatroom.response.ChatRoomListResponseDto; +import inu.codin.codin.domain.chat.service.ChatRoomService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -17,7 +17,7 @@ @RequiredArgsConstructor @Slf4j @RequestMapping("/chatroom") -@Tag(name = "ChatRoom API", description = "채팅방 생성, 리스트 반환, 채팅방 나가기, 채팅방 알림 설정") +@Tag(name = "ChatRoom API", description = "채팅방 생성, 조회, 삭제, 채팅방 알림 설정") public class ChatRoomController { private final ChatRoomService chatRoomService; @@ -51,7 +51,7 @@ public ResponseEntity> leaveChatRoom(@PathVariable("chatRoomId } @Operation( - summary = "채팅방 알림 여부 수정" + summary = "채팅방 알림 여부 토글 수정" ) @GetMapping("/notification/{chatRoomId}") public ResponseEntity> setNotificationChatRoom(@PathVariable("chatRoomId") String chatRoomId){ diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ChatRoom.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ChatRoom.java similarity index 76% rename from src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ChatRoom.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ChatRoom.java index e7767a05..ac836bd4 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/entity/ChatRoom.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ChatRoom.java @@ -1,7 +1,7 @@ -package inu.codin.codin.domain.chat.chatroom.entity; +package inu.codin.codin.domain.chat.domain.chatroom; import inu.codin.codin.common.dto.BaseTimeEntity; -import inu.codin.codin.domain.chat.chatroom.dto.ChatRoomCreateRequestDto; +import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; import jakarta.validation.constraints.NotBlank; import lombok.AccessLevel; import lombok.Builder; @@ -25,7 +25,7 @@ public class ChatRoom extends BaseTimeEntity { private String roomName; @NotBlank - private ObjectId referenceId; //채팅방이 시작한 곳의 id + private ObjectId referenceId; //채팅방이 시작한 곳의 id (게시글, 댓글, 대댓글 _id) @NotBlank private Participants participants; //참가자들의 userId (1:1 채팅에서는 두 명의 id만 들어감) @@ -45,17 +45,22 @@ public ChatRoom(String roomName, ObjectId referenceId, Participants participants } public static ChatRoom of(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId){ - Participants participants = new Participants(); - participants.create(senderId); - participants.create(new ObjectId(chatRoomCreateRequestDto.getReceiverId())); + Participants participants = getParticipants(chatRoomCreateRequestDto, senderId); return ChatRoom.builder() .roomName(chatRoomCreateRequestDto.getRoomName()) - .referenceId(new ObjectId(chatRoomCreateRequestDto.getReferenceId())) + .referenceId(chatRoomCreateRequestDto.getReferenceId()) .participants(participants) .currentMessageDate(LocalDateTime.now()) .build(); } + private static Participants getParticipants(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { + Participants participants = new Participants(); + participants.create(senderId); + participants.create(chatRoomCreateRequestDto.getReceiverId()); + return participants; + } + public void updateLastMessage(String message){ this.lastMessage = message; this.currentMessageDate = LocalDateTime.now(); diff --git a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java new file mode 100644 index 00000000..54c3a979 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java @@ -0,0 +1,61 @@ +package inu.codin.codin.domain.chat.domain.chatroom; + +import inu.codin.codin.common.dto.BaseTimeEntity; +import lombok.*; +import org.bson.types.ObjectId; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +public class ParticipantInfo extends BaseTimeEntity { + + private ObjectId userId; + private boolean isConnected = false; + private int unreadCount = 0; + private boolean isLeaved = false; + private LocalDateTime whenLeaved; + private boolean notificationsEnabled = true; + + public ParticipantInfo(ObjectId userId) { + this.userId = userId; + } + + public static ParticipantInfo enter(ObjectId userId) { + return new ParticipantInfo(userId); + } + + public void updateNotification() { + this.notificationsEnabled = !notificationsEnabled; + } + + public void incrementUnreadCount() { + this.unreadCount++; + } + + public void connect() { + this.isConnected = true; + this.unreadCount = 0; + } + + public void disconnect() { + this.isConnected = false; + this.unreadCount = 0; + setUpdatedAt(); + } + + public void leave() { + this.isLeaved = true; + this.whenLeaved = LocalDateTime.now(); + disconnect(); + } + + public void remain() { + this.isLeaved = false; + } + + public boolean isNotified(ObjectId userId){ + return !this.userId.equals(userId) && this.notificationsEnabled & !this.isConnected; + } + +} diff --git a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java new file mode 100644 index 00000000..5d0aa627 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java @@ -0,0 +1,97 @@ +package inu.codin.codin.domain.chat.domain.chatroom; + +import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; +import inu.codin.codin.domain.chat.exception.ChatRoomException; +import lombok.Getter; +import lombok.Setter; +import org.bson.types.ObjectId; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Getter +@Setter +public class Participants { + + private Map info = new ConcurrentHashMap<>(); + + public void create(ObjectId memberId){ + info.put(memberId, ParticipantInfo.enter(memberId)); + } + + /** + * 메세지를 받는 유저가 끊겨 있는 상태라면 unreadCount 업데이트 후 해당 유저 정보 반환 + * @param senderId 메세지를 보내는 유저 id + * @return 메세지를 받는 유저들의 정보 리스트 + */ + public List getDisconnectedUsersAndUpdateUnreadCount(ObjectId senderId) { + return info.keySet().stream().map(receiverId -> { + if (!receiverId.equals(senderId)) { + ParticipantInfo receiverInfo = info.get(receiverId); + if (!receiverInfo.isConnected()){ + receiverInfo.incrementUnreadCount(); + return new ReceiverInfo(receiverId, receiverInfo.getUnreadCount()); + } + } + return null; + }).filter(Objects::nonNull).toList(); + } + + public void enter(ObjectId userId) { + findParticipant(userId).connect(); + } + + public void exit(ObjectId userId) { + findParticipant(userId).disconnect(); + } + + public void leave(ObjectId userId) { + findParticipant(userId).leave(); + } + + public void toggleNotification(ObjectId userId) { + findParticipant(userId).updateNotification(); + } + + public List getUsersToNotify(ObjectId senderId) { + return info.values().stream() + .filter(participantInfo -> participantInfo.isNotified(senderId)) + .map(ParticipantInfo::getUserId).toList(); + } + + public LocalDateTime getWhenLeaved(ObjectId userId) { + return findParticipant(userId).getWhenLeaved(); + } + + public List remainReceiver(ObjectId userId) { + return info.values().stream() + .filter(info -> !info.getUserId().equals(userId) && info.isLeaved()) + .peek(ParticipantInfo::remain) + .toList(); + } + + public int getCountOfConnecting() { + return (int) info.values().stream() + .filter(ParticipantInfo::isConnected) + .count(); + } + + public int size() { + return info.size(); + } + + public int getUnreadCount(ObjectId userId) { + return findParticipant(userId).getUnreadCount(); + } + + private ParticipantInfo findParticipant(ObjectId userId) { + return Optional.ofNullable(info.get(userId)) + .orElseThrow(() -> new ChatRoomException(ChatRoomErrorCode.PARTICIPANTS_NOT_FOUND)); + } + + public record ReceiverInfo(ObjectId receiverId, int unreadMessage){} +} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/dto/event/ChatRoomNotificationEvent.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/event/ChatRoomNotificationEvent.java similarity index 82% rename from src/main/java/inu/codin/codin/domain/chat/chatroom/dto/event/ChatRoomNotificationEvent.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatroom/event/ChatRoomNotificationEvent.java index b3c9695a..ac051004 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/dto/event/ChatRoomNotificationEvent.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/event/ChatRoomNotificationEvent.java @@ -1,6 +1,6 @@ -package inu.codin.codin.domain.chat.chatroom.dto.event; +package inu.codin.codin.domain.chat.domain.chatroom.event; -import inu.codin.codin.domain.chat.chatroom.entity.Participants; +import inu.codin.codin.domain.chat.domain.chatroom.Participants; import lombok.Getter; import org.bson.types.ObjectId; import org.springframework.context.ApplicationEvent; diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/dto/ChatRoomCreateRequestDto.java b/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/request/ChatRoomCreateRequestDto.java similarity index 64% rename from src/main/java/inu/codin/codin/domain/chat/chatroom/dto/ChatRoomCreateRequestDto.java rename to src/main/java/inu/codin/codin/domain/chat/dto/chatroom/request/ChatRoomCreateRequestDto.java index ac998c98..eea12a9b 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/dto/ChatRoomCreateRequestDto.java +++ b/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/request/ChatRoomCreateRequestDto.java @@ -1,12 +1,13 @@ -package inu.codin.codin.domain.chat.chatroom.dto; +package inu.codin.codin.domain.chat.dto.chatroom.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; +import org.bson.types.ObjectId; @Getter -@Setter +@Builder public class ChatRoomCreateRequestDto { @NotBlank @@ -20,4 +21,12 @@ public class ChatRoomCreateRequestDto { @NotBlank @Schema(description = "채팅이 시작된 게시글, 댓글, 댓글의 id", example = "65asdf") private String referenceId; + + public ObjectId getReceiverId(){ + return new ObjectId(this.receiverId); + } + + public ObjectId getReferenceId(){ + return new ObjectId(this.referenceId); + } } diff --git a/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomCreateResponseDto.java b/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomCreateResponseDto.java new file mode 100644 index 00000000..a214222b --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomCreateResponseDto.java @@ -0,0 +1,15 @@ +package inu.codin.codin.domain.chat.dto.chatroom.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class ChatRoomCreateResponseDto { + + @Schema(description = "생성된 채팅방 _id", example = "111111") + private final String chatRoomId; + + public ChatRoomCreateResponseDto(String chatRoomId) { + this.chatRoomId = chatRoomId; + } +} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/dto/ChatRoomListResponseDto.java b/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomListResponseDto.java similarity index 93% rename from src/main/java/inu/codin/codin/domain/chat/chatroom/dto/ChatRoomListResponseDto.java rename to src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomListResponseDto.java index ebdcb985..57e63697 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/dto/ChatRoomListResponseDto.java +++ b/src/main/java/inu/codin/codin/domain/chat/dto/chatroom/response/ChatRoomListResponseDto.java @@ -1,7 +1,7 @@ -package inu.codin.codin.domain.chat.chatroom.dto; +package inu.codin.codin.domain.chat.dto.chatroom.response; import com.fasterxml.jackson.annotation.JsonFormat; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Builder; @@ -48,7 +48,7 @@ public static ChatRoomListResponseDto of(ChatRoom chatRoom, ObjectId userId) { .roomName(chatRoom.getRoomName()) .lastMessage(chatRoom.getLastMessage()==null ? null : chatRoom.getLastMessage()) .currentMessageDate(chatRoom.getCurrentMessageDate()==null ? null : chatRoom.getCurrentMessageDate()) - .unread(chatRoom.getParticipants().getInfo().get(userId).getUnreadMessage()) + .unread(chatRoom.getParticipants().getInfo().get(userId).getUnreadCount()) .build(); } } diff --git a/src/main/java/inu/codin/codin/domain/chat/event/ChatRoomEventListener.java b/src/main/java/inu/codin/codin/domain/chat/event/ChatRoomEventListener.java new file mode 100644 index 00000000..6d32cd85 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/event/ChatRoomEventListener.java @@ -0,0 +1,29 @@ +package inu.codin.codin.domain.chat.event; + +import inu.codin.codin.domain.chat.domain.chatroom.ParticipantInfo; +import inu.codin.codin.domain.chat.domain.chatroom.event.ChatRoomNotificationEvent; +import inu.codin.codin.domain.notification.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ChatRoomEventListener { + + private final NotificationService notificationService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleChatRoomNotification(ChatRoomNotificationEvent event){ + Optional.ofNullable(event.getParticipants().getInfo().get(event.getReceiverId())) + .filter(ParticipantInfo::isNotificationsEnabled) + .ifPresent(participant -> + notificationService.sendNotificationMessageByChat(participant.getUserId(), event.getChatRoomId()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/inu/codin/codin/domain/chat/exception/ChatRoomExistedException.java b/src/main/java/inu/codin/codin/domain/chat/exception/ChatRoomExistedException.java new file mode 100644 index 00000000..fc9389ad --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/exception/ChatRoomExistedException.java @@ -0,0 +1,15 @@ +package inu.codin.codin.domain.chat.exception; + +import lombok.Getter; +import org.bson.types.ObjectId; + +@Getter +public class ChatRoomExistedException extends ChatRoomException{ + + private final ObjectId chatRoomId; + + public ChatRoomExistedException(ObjectId chatRoomId){ + super(ChatRoomErrorCode.CHATROOM_EXISTED); + this.chatRoomId = chatRoomId; + } +} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatroom/repository/ChatRoomRepository.java b/src/main/java/inu/codin/codin/domain/chat/repository/ChatRoomRepository.java similarity index 77% rename from src/main/java/inu/codin/codin/domain/chat/chatroom/repository/ChatRoomRepository.java rename to src/main/java/inu/codin/codin/domain/chat/repository/ChatRoomRepository.java index d3db3f56..d60fcadd 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatroom/repository/ChatRoomRepository.java +++ b/src/main/java/inu/codin/codin/domain/chat/repository/ChatRoomRepository.java @@ -1,6 +1,6 @@ -package inu.codin.codin.domain.chat.chatroom.repository; +package inu.codin.codin.domain.chat.repository; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; import org.bson.types.ObjectId; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; @@ -10,8 +10,7 @@ public interface ChatRoomRepository extends MongoRepository { - @Query("{ '_id': ?0, 'deletedAt': null }") - Optional findById(ObjectId id); + Optional findBy_idAndDeletedAtIsNull(ObjectId id); @Query("{ 'participants.info.?0.userId': ?0, 'participants.info.?0.isLeaved': false, 'deletedAt': null }") List findByParticipantIsNotLeavedAndDeletedIsNull(ObjectId userId); diff --git a/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomService.java b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomService.java new file mode 100644 index 00000000..8801d62f --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomService.java @@ -0,0 +1,97 @@ +package inu.codin.codin.domain.chat.service; + +import inu.codin.codin.common.security.util.SecurityUtils; +import inu.codin.codin.domain.block.service.BlockService; +import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; +import inu.codin.codin.domain.chat.dto.chatroom.response.ChatRoomCreateResponseDto; +import inu.codin.codin.domain.chat.dto.chatroom.response.ChatRoomListResponseDto; +import inu.codin.codin.domain.chat.domain.chatroom.event.ChatRoomNotificationEvent; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; +import inu.codin.codin.domain.chat.exception.ChatRoomException; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatRoomService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomValidator chatRoomValidator; + private final BlockService blockService; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public ChatRoomCreateResponseDto createChatRoom(ChatRoomCreateRequestDto chatRoomCreateRequestDto) { + ObjectId senderId = SecurityUtils.getCurrentUserId(); + + chatRoomValidator.validate(chatRoomCreateRequestDto, senderId); + + log.info("[채팅방 생성 요청] 송신자 ID: {}, 수신자 ID: {}", senderId, chatRoomCreateRequestDto.getReceiverId()); + ChatRoom chatRoom = ChatRoom.of(chatRoomCreateRequestDto, senderId); + chatRoomRepository.save(chatRoom); + log.info("[채팅방 생성 완료] 채팅방 ID: {}, 송신자 ID: {}, 수신자 ID: {}", chatRoom.get_id(), senderId, chatRoomCreateRequestDto.getReceiverId()); + + eventPublisher.publishEvent(new ChatRoomNotificationEvent( + this, chatRoom.get_id(), chatRoomCreateRequestDto.getReceiverId(), chatRoom.getParticipants())); + + return new ChatRoomCreateResponseDto(chatRoom.get_id().toString()); + } + + public List getAllChatRoomByUser() { + ObjectId userId = SecurityUtils.getCurrentUserId(); + log.info("[유저의 채팅방 조회] 유저 ID: {}", userId); + + // 차단 목록 조회 + List blockedUsersId = blockService.getBlockedUsers(); + List chatRooms = chatRoomRepository.findByParticipantIsNotLeavedAndDeletedIsNull(userId); + log.info("[채팅방 조회 결과] 유저 ID: {}가 참여 중인 채팅방 개수: {}", userId, chatRooms.size()); + return chatRooms.stream() + .filter(chatRoom -> chatRoom.getParticipants().getInfo().keySet().stream() + .noneMatch(blockedUsersId::contains)) + .sorted(Comparator.comparing(ChatRoom::getCurrentMessageDate,Comparator.nullsLast(Comparator.reverseOrder()))) + .map(chatRoom -> ChatRoomListResponseDto.of(chatRoom, userId)).toList(); + } + + @Transactional + public void leaveChatRoom(String chatRoomId) { + ObjectId userId = SecurityUtils.getCurrentUserId(); + log.info("[채팅방 나가기 요청] 유저 ID: {}, 채팅방 ID: {}", userId, chatRoomId); + + ChatRoom chatRoom = getChatRoom(chatRoomId); + chatRoom.getParticipants().leave(userId); + chatRoomRepository.save(chatRoom); + log.info("[채팅방 나가기 완료] 유저 ID: {}, 채팅방 ID: {}", userId, chatRoomId); + + //채팅방 참여자가 모두 나갔는지 확인 + chatRoomValidator.validateParticipantsAllLeaved(chatRoomId, chatRoom); + } + + @Transactional + public void setNotificationChatRoom(String chatRoomId) { + ObjectId userId = SecurityUtils.getCurrentUserId(); + log.info("[알림 설정 요청] 유저 ID: {}, 채팅방 ID: {}", userId, chatRoomId); + + ChatRoom chatRoom = getChatRoom(chatRoomId); + chatRoom.getParticipants().toggleNotification(userId); + chatRoomRepository.save(chatRoom); + log.info("[알림 설정 완료] 채팅방 ID: {}에 알림 설정 완료", chatRoomId); + } + + public ChatRoom getChatRoom(String chatRoomId) { + return chatRoomRepository.findBy_idAndDeletedAtIsNull(new ObjectId(chatRoomId)) + .orElseThrow(() -> { + log.error("[채팅방 확인 실패] 채팅방 ID: {}를 찾을 수 없습니다.", chatRoomId); + return new ChatRoomException(ChatRoomErrorCode.CHATROOM_NOT_FOUND); + }); + } + +} diff --git a/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java new file mode 100644 index 00000000..5cff9f40 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java @@ -0,0 +1,70 @@ +package inu.codin.codin.domain.chat.service; + +import inu.codin.codin.common.exception.NotFoundException; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.ParticipantInfo; +import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; +import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; +import inu.codin.codin.domain.chat.exception.ChatRoomException; +import inu.codin.codin.domain.chat.exception.ChatRoomExistedException; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import inu.codin.codin.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatRoomValidator { + + private final ChatRoomRepository chatRoomRepository; + private final UserRepository userRepository; + + public void validate(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { + validateReceiverExisted(chatRoomCreateRequestDto); //Receiver가 존재하는 지 학인 + validateChatRoomDuplicated(chatRoomCreateRequestDto, senderId); //동일한 채팅방이 있는지 확인 + validateNotSelfChat(chatRoomCreateRequestDto, senderId); //자기 자신과 채팅방을 만들었는지 확인 + } + + private void validateNotSelfChat(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { + if (senderId.equals(chatRoomCreateRequestDto.getReceiverId())) + throw new ChatRoomException(ChatRoomErrorCode.CHATROOM_EXISTED); + } + + private void validateChatRoomDuplicated(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { + Optional existedChatroom = chatRoomRepository.findByReferenceIdAndParticipantsContaining( + chatRoomCreateRequestDto.getReferenceId(), senderId, chatRoomCreateRequestDto.getReceiverId()); + if (existedChatroom.isPresent()) { + ParticipantInfo participantInfo= existedChatroom.get().getParticipants().getInfo().get(senderId); + if (participantInfo.isLeaved()) { + participantInfo.remain(); + chatRoomRepository.save(existedChatroom.get()); + } + //client 측에서 303 에러를 받아서 해당 채팅방으로 redirect + throw new ChatRoomExistedException(existedChatroom.get().get_id()); + } + } + + private void validateReceiverExisted(ChatRoomCreateRequestDto chatRoomCreateRequestDto) { + userRepository.findById(chatRoomCreateRequestDto.getReceiverId()) + .orElseThrow(() -> { + log.error("[Receive 유저 확인 실패] 수신자 ID: {}를 찾을 수 없습니다.", chatRoomCreateRequestDto.getReceiverId()); + return new NotFoundException("Receive 유저를 찾을 수 없습니다."); + }); + } + + public void validateParticipantsAllLeaved(String chatRoomId, ChatRoom chatRoom) { + boolean isAllLeaved = chatRoom.getParticipants().getInfo().values() + .stream() + .allMatch(ParticipantInfo::isLeaved); // 모든 참가자가 떠났는지 확인 + if (isAllLeaved) { + chatRoom.delete(); + chatRoomRepository.save(chatRoom); + log.info("[채팅방 삭제] 채팅방 ID: {}에 더 이상 참여자가 없어 채팅방을 삭제합니다.", chatRoomId); + } + } +} From 8c51f732cd577d9d02130c43ec9fdef98a19db50 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Sat, 28 Jun 2025 00:17:05 +0900 Subject: [PATCH 02/15] =?UTF-8?q?refactor=20:=20Chatting=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codin/common/config/FeignConfig.java | 14 -- .../chat/chatting/entity/MessageType.java | 10 -- .../exception/ChattingNotFoundException.java | 7 - .../repository/CustomChattingRepository.java | 29 ---- .../service/ChattingEventListener.java | 110 ------------- .../chatting/service/ChattingService.java | 112 -------------- .../domain/chat/config/WebSocketConfig.java | 55 +++++++ .../controller/ChattingController.java | 44 ++---- .../controller/ChattingMvcController.java | 24 +++ .../entity => domain/chatting}/Chatting.java | 5 +- .../dto => domain/chatting}/ContentType.java | 2 +- .../chatting}/event/ChattingArrivedEvent.java | 9 +- .../event/ChattingNotificationEvent.java | 4 +- .../event/ChattingUnreadCountEvent.java} | 8 +- .../chatting}/request/ChattingRequestDto.java | 9 +- .../ChattingAndUserIdResponseDto.java | 6 +- .../response/ChattingResponseDto.java | 23 +-- .../chat/event/ChattingEventListener.java | 100 ++++++++++++ .../repository/ChattingRepository.java | 6 +- .../domain/chat/service/ChattingService.java | 113 ++++++++++++++ .../chat/stomp/HttpHandShakeInterceptor.java | 47 ++++++ .../chat/stomp/StompMessageProcessor.java | 48 ++++++ .../chat/stomp/StompMessageService.java | 144 ++++++++++++++++++ 23 files changed, 576 insertions(+), 353 deletions(-) delete mode 100644 src/main/java/inu/codin/codin/common/config/FeignConfig.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatting/entity/MessageType.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatting/exception/ChattingNotFoundException.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatting/repository/CustomChattingRepository.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingEventListener.java delete mode 100644 src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingService.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/config/WebSocketConfig.java rename src/main/java/inu/codin/codin/domain/chat/{chatting => }/controller/ChattingController.java (58%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/controller/ChattingMvcController.java rename src/main/java/inu/codin/codin/domain/chat/{chatting/entity => domain/chatting}/Chatting.java (89%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto => domain/chatting}/ContentType.java (58%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto => domain/chatting}/event/ChattingArrivedEvent.java (53%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto => domain/chatting}/event/ChattingNotificationEvent.java (78%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto/event/UpdateUnreadCountEvent.java => domain/chatting/event/ChattingUnreadCountEvent.java} (51%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto => dto/chatting}/request/ChattingRequestDto.java (61%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto => dto/chatting}/response/ChattingAndUserIdResponseDto.java (67%) rename src/main/java/inu/codin/codin/domain/chat/{chatting/dto => dto/chatting}/response/ChattingResponseDto.java (70%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java rename src/main/java/inu/codin/codin/domain/chat/{chatting => }/repository/ChattingRepository.java (78%) create mode 100644 src/main/java/inu/codin/codin/domain/chat/service/ChattingService.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/stomp/HttpHandShakeInterceptor.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageProcessor.java create mode 100644 src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java diff --git a/src/main/java/inu/codin/codin/common/config/FeignConfig.java b/src/main/java/inu/codin/codin/common/config/FeignConfig.java deleted file mode 100644 index 38d348e8..00000000 --- a/src/main/java/inu/codin/codin/common/config/FeignConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package inu.codin.codin.common.config; - -import feign.Client; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class FeignConfig { - - @Bean - public Client feignClient(){ - return new Client.Default(null, null); - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/entity/MessageType.java b/src/main/java/inu/codin/codin/domain/chat/chatting/entity/MessageType.java deleted file mode 100644 index 505b176e..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/entity/MessageType.java +++ /dev/null @@ -1,10 +0,0 @@ -package inu.codin.codin.domain.chat.chatting.entity; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public enum MessageType { - SEND, - EXIT - //추후 추가 예정 -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/exception/ChattingNotFoundException.java b/src/main/java/inu/codin/codin/domain/chat/chatting/exception/ChattingNotFoundException.java deleted file mode 100644 index 5d1f296c..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/exception/ChattingNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package inu.codin.codin.domain.chat.chatting.exception; - -public class ChattingNotFoundException extends RuntimeException{ - public ChattingNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/repository/CustomChattingRepository.java b/src/main/java/inu/codin/codin/domain/chat/chatting/repository/CustomChattingRepository.java deleted file mode 100644 index 0aa7e48c..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/repository/CustomChattingRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package inu.codin.codin.domain.chat.chatting.repository; - -import inu.codin.codin.domain.chat.chatting.entity.Chatting; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.ReactiveMongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Mono; - -@Repository -public class CustomChattingRepository { - - private final MongoTemplate mongoTemplate; - - public CustomChattingRepository(MongoTemplate mongoTemplate) { - this.mongoTemplate = mongoTemplate; - } - - public Chatting findMostRecentByChatRoomId(ObjectId chatRoomId) { - Query query = new Query(Criteria.where("chatRoomId").is(chatRoomId)) - .with(Sort.by(Sort.Direction.DESC, "createdAt")) - .limit(1); - return mongoTemplate.findOne(query, Chatting.class); - } - -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingEventListener.java b/src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingEventListener.java deleted file mode 100644 index ca6a5f2c..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingEventListener.java +++ /dev/null @@ -1,110 +0,0 @@ -package inu.codin.codin.domain.chat.chatting.service; - -import inu.codin.codin.common.exception.NotFoundException; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; -import inu.codin.codin.domain.chat.chatroom.entity.ParticipantInfo; -import inu.codin.codin.domain.chat.chatroom.repository.ChatRoomRepository; -import inu.codin.codin.domain.chat.chatting.dto.event.ChattingArrivedEvent; -import inu.codin.codin.domain.chat.chatting.dto.event.ChattingNotificationEvent; -import inu.codin.codin.domain.chat.chatting.dto.event.UpdateUnreadCountEvent; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; -import inu.codin.codin.domain.chat.chatting.repository.ChattingRepository; -import inu.codin.codin.domain.notification.service.NotificationService; -import inu.codin.codin.domain.user.entity.UserEntity; -import inu.codin.codin.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.context.event.EventListener; -import org.springframework.messaging.simp.SimpMessageSendingOperations; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.util.*; - -@Service -@RequiredArgsConstructor -public class ChattingEventListener { - - private final ChatRoomRepository chatRoomRepository; - private final ChattingRepository chattingRepository; - private final UserRepository userRepository; - private final SimpMessageSendingOperations template; - private final NotificationService notificationService; - - /* - 채팅을 발신했을 경우, - 1. 상대방이 접속한 상태가 아니라면 상대방의 unread 값 +1 - 2. 채팅방의 마지막 메세지 업데이트 - 3. /queue/chatroom/unread 를 통해 상대방의 채팅방 목록 실시간 업데이트 - */ - @Async - @EventListener - public void handleChattingArrivedEvent(ChattingArrivedEvent event){ - Chatting chatting = event.getChatting(); - ChatRoom chatRoom = chatRoomRepository.findById(chatting.getChatRoomId()) - .orElseThrow(() -> new NotFoundException("채팅방을 찾을 수 없습니다. ID: "+ chatting.getChatRoomId())); - - updateUnread(event, chatRoom); - chatRoomRepository.save(chatRoom); - chattingRepository.save(chatting); - - } - - private void updateUnread(ChattingArrivedEvent event, ChatRoom chatRoom) { - Map result = new HashMap<>(); - ObjectId receiverId = null; - for (Map.Entry entry : chatRoom.getParticipants().getInfo().entrySet()) { - ParticipantInfo participantInfo = entry.getValue(); - - if (!participantInfo.getUserId().equals(event.getChatting().getSenderId())) { - if (!participantInfo.isConnected()) { - receiverId = participantInfo.getUserId(); - participantInfo.plusUnread(); - result = getLastMessageAndUnread(event, participantInfo); - } - } - } - chatRoom.updateLastMessage(event.getChatting().getContent()); - if (receiverId!=null) { //받는 사람이 없다는 것은 채팅에 연결 중인 상태, 채팅방 업데이트할 필요 X - Optional user = userRepository.findByUserId(receiverId); - if (user.isPresent()) - template.convertAndSendToUser(user.get().getEmail(), "/queue/chatroom/unread", result); - } - } - - private static Map getLastMessageAndUnread(ChattingArrivedEvent event, ParticipantInfo participantInfo) { - return Map.of( - "chatRoomId", event.getChatting().getChatRoomId().toString(), - "lastMessage", event.getChatting().getContent(), - "unread", String.valueOf(participantInfo.getUnreadMessage()) - ); - } - - @Async - @EventListener - public void handleChattingNotificationEvent(ChattingNotificationEvent event){ - event.getChatRoom().getParticipants().getInfo().values().stream() - .filter(participantInfo -> !participantInfo.getUserId().equals(event.getUserId()) && participantInfo.isNotificationsEnabled() & !participantInfo.isConnected()) - .peek(participantInfo -> notificationService.sendNotificationMessageByChat(participantInfo.getUserId(), event.getChatRoom().get_id())); - } - - /* - 유저가 채팅방 입장 시, 읽지 않은 채팅에 대하여 새로운 unread 값 송신 - 클라이언트 : chat_id 와 일치하는 채팅값의 unread 값 업데이트 - */ - @EventListener - public void updateUnreadCountEvent(UpdateUnreadCountEvent updateUnreadCountEvent){ - List> result = new ArrayList<>(); - for (Chatting chat : updateUnreadCountEvent.getChattingList()){ - Map payload = Map.of( - "id", chat.get_id().toString(), - "unread", String.valueOf(chat.getUnreadCount()) - ); - result.add(payload); - } - - template.convertAndSend("/queue/unread/"+ updateUnreadCountEvent.getChatRoomId(), result); - - } - -} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingService.java b/src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingService.java deleted file mode 100644 index 5c3af31a..00000000 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/service/ChattingService.java +++ /dev/null @@ -1,112 +0,0 @@ -package inu.codin.codin.domain.chat.chatting.service; - -import inu.codin.codin.common.security.util.SecurityUtils; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; -import inu.codin.codin.domain.chat.chatroom.entity.ParticipantInfo; -import inu.codin.codin.domain.chat.chatroom.exception.ChatRoomNotFoundException; -import inu.codin.codin.domain.chat.chatroom.repository.ChatRoomRepository; -import inu.codin.codin.domain.chat.chatroom.service.ChatRoomService; -import inu.codin.codin.domain.chat.chatting.dto.event.ChattingArrivedEvent; -import inu.codin.codin.domain.chat.chatting.dto.event.ChattingNotificationEvent; -import inu.codin.codin.domain.chat.chatting.dto.request.ChattingRequestDto; -import inu.codin.codin.domain.chat.chatting.dto.response.ChattingAndUserIdResponseDto; -import inu.codin.codin.domain.chat.chatting.dto.response.ChattingResponseDto; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; -import inu.codin.codin.domain.chat.chatting.repository.ChattingRepository; -import inu.codin.codin.domain.notification.service.NotificationService; -import inu.codin.codin.domain.user.security.CustomUserDetails; -import inu.codin.codin.infra.s3.S3Service; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.bson.types.ObjectId; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.time.LocalDateTime; -import java.util.List; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ChattingService { - - private final ChatRoomRepository chatRoomRepository; - private final ChattingRepository chattingRepository; - private final S3Service s3Service; - private final NotificationService notificationService; - private final ChatRoomService chatRoomService; - private final ApplicationEventPublisher eventPublisher; - - public ChattingResponseDto sendMessage(String id, ChattingRequestDto chattingRequestDto, Authentication authentication) { - ChatRoom chatRoom = chatRoomRepository.findById(new ObjectId(id)) - .orElseThrow(() -> { - log.warn("[채팅방 조회 실패] 채팅방 ID: {}를 찾을 수 없습니다.", id); - return new ChatRoomNotFoundException("채팅방을 찾을 수 없습니다."); - }); - ObjectId userId = ((CustomUserDetails) authentication.getPrincipal()).getId(); - Integer countOfParticipating = chatRoomService.countOfParticipating(chatRoom.get_id()); //접속해 있는 사람 수 빼기 (읽은 count) - Chatting chatting = Chatting.of(chatRoom.get_id(), chattingRequestDto, userId, - chatRoom.getParticipants().getInfo().size()-countOfParticipating); - - log.info("[메시지 전송 성공] 메시지: [{}], 송신자 ID: {}, 채팅방 ID: {}", chattingRequestDto.getContent(), userId, id); - - chattingRepository.save(chatting); - - //상대가 채팅방을 나간 상태라면 다시 불러와서 채팅 시작 - chatRoom.getParticipants().getInfo().values().stream() - .filter(info -> !info.getUserId().equals(userId) && info.isLeaved()) - .forEach(ParticipantInfo::remain); - chatRoomRepository.save(chatRoom); - - //상대 유저가 접속하지 않은 상태라면 unread 개수 업데이트 및 마지막 대화 내용 업데이트 - eventPublisher.publishEvent(new ChattingArrivedEvent(this, chatting)); - //알림 보내기 - eventPublisher.publishEvent(new ChattingNotificationEvent(this, userId, chatRoom)); - - return ChattingResponseDto.of(chatting); - } - - public ChattingAndUserIdResponseDto getAllMessage(String id, int page) { - ObjectId userId = SecurityUtils.getCurrentUserId(); - ChatRoom chatRoom = chatRoomRepository.findById(new ObjectId(id)) - .orElseThrow(() -> { - log.warn("[채팅방 조회 실패] 채팅방 ID: {}를 찾을 수 없습니다.", id); - return new ChatRoomNotFoundException("채팅방을 찾을 수 없습니다."); - }); - - Pageable pageable = PageRequest.of(page, 20, Sort.by("createdAt").descending()); - chatRoomRepository.findById(new ObjectId(id)) - .orElseThrow(() -> { - log.error("[채팅방 조회 실패] 채팅방 ID: {}를 찾을 수 없습니다.", id); - return new ChatRoomNotFoundException("채팅방을 찾을 수 없습니다."); - }); - - List chattingResponseDto; - LocalDateTime whenLeaved = chatRoom.getParticipants().getInfo().get(userId).getWhenLeaved(); - if (whenLeaved!= null) //나간 적이 있다면 그 이후의 채팅 내역만 반환 - chattingResponseDto = chattingRepository.findAllByChatRoomIdAndCreatedAtAfter(new ObjectId(id), whenLeaved, pageable) - .stream().map(ChattingResponseDto::of).toList(); - else chattingResponseDto = chattingRepository.findAllByChatRoomId(new ObjectId(id), pageable) - .stream().map(ChattingResponseDto::of).toList(); - - - log.info("[메시지 조회 성공] 채팅방 ID: {}, 메시지 개수: {}", id, chattingResponseDto.size()); - - return new ChattingAndUserIdResponseDto(chattingResponseDto, SecurityUtils.getCurrentUserId().toString()); - } - - public List sendImageMessage(List chatImages) { - log.info("[이미지 메시지 전송] 이미지 개수: {}", chatImages.size()); - - List imageUrls = s3Service.handleImageUpload(chatImages); - - log.info("[이미지 메시지 전송 성공] 업로드된 이미지 URL 개수: {}", imageUrls.size()); - - return imageUrls; - } -} diff --git a/src/main/java/inu/codin/codin/domain/chat/config/WebSocketConfig.java b/src/main/java/inu/codin/codin/domain/chat/config/WebSocketConfig.java new file mode 100644 index 00000000..3f0ada5a --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/config/WebSocketConfig.java @@ -0,0 +1,55 @@ +package inu.codin.codin.domain.chat.config; + +import inu.codin.codin.common.security.service.JwtService; +import inu.codin.codin.domain.chat.stomp.HttpHandShakeInterceptor; +import inu.codin.codin.domain.chat.stomp.StompMessageProcessor; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +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; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; + +@Configuration +@RequiredArgsConstructor +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Value("${server.domain}") + private String BASEURL; + + private final StompMessageProcessor stompMessageProcessor; + private final JwtService jwtService; + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws-stomp") //handshake endpoint + .setAllowedOriginPatterns("http://localhost:3000", "http://localhost:8080", BASEURL) + .withSockJS() + .setInterceptors(new HttpHandShakeInterceptor(jwtService)); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/topic", "/queue"); + //해당 주소를 구독 및 구독하고 있는 클라이언트들에게 메세지 전달 + //메세지를 브로커로 라우팅 + registry.setApplicationDestinationPrefixes("/pub"); + //클라이언트에서 보낸 메세지를 받을 prefix, controller의 @MessageMapping과 이어짐 + registry.setUserDestinationPrefix("/user"); + //convertAndSendToUser 사용할 prefix + } + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registration) { + registration.setMessageSizeLimit(50 * 1024 * 1024); // 메세지 크기 제한 오류 방지(이 코드가 없으면 byte code를 보낼때 소켓 연결이 끊길 수 있음) + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompMessageProcessor); + } +} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/controller/ChattingController.java b/src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java similarity index 58% rename from src/main/java/inu/codin/codin/domain/chat/chatting/controller/ChattingController.java rename to src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java index 53f45c62..34439fc6 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/controller/ChattingController.java +++ b/src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java @@ -1,8 +1,8 @@ -package inu.codin.codin.domain.chat.chatting.controller; +package inu.codin.codin.domain.chat.controller; import inu.codin.codin.common.response.SingleResponse; -import inu.codin.codin.domain.chat.chatting.dto.request.ChattingRequestDto; -import inu.codin.codin.domain.chat.chatting.service.ChattingService; +import inu.codin.codin.domain.chat.dto.chatting.request.ChattingRequestDto; +import inu.codin.codin.domain.chat.service.ChattingService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -14,14 +14,14 @@ import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; -@Controller +@RestController @RequiredArgsConstructor +@RequestMapping("/chats") @Tag(name = "Chatting API", description = "채팅 보내기, 채팅 내역 반환") public class ChattingController { @@ -30,17 +30,18 @@ public class ChattingController { @Operation( summary = "채팅 보내기" ) - @MessageMapping("/chats/{chatRoomId}") //앞에 '/pub' 를 붙여서 요청 + @MessageMapping("/chats/{chatRoomId}") //client 측에서 앞에 '/pub' 를 붙여서 요청 @SendTo("/queue/{chatRoomId}") - public ResponseEntity> sendMessage(@DestinationVariable("chatRoomId") String id, @RequestBody @Valid ChattingRequestDto chattingRequestDto, + public ResponseEntity> sendMessage(@DestinationVariable("chatRoomId") String chatRoomId, @RequestBody @Valid ChattingRequestDto chattingRequestDto, @AuthenticationPrincipal Authentication authentication){ - return ResponseEntity.ok().body(new SingleResponse<>(200, "채팅 송신 완료", chattingService.sendMessage(id, chattingRequestDto, authentication))); + return ResponseEntity.ok() + .body(new SingleResponse<>(200, "채팅 송신 완료", chattingService.sendMessage(chatRoomId, chattingRequestDto, authentication))); } @Operation( summary = "채팅으로 사진 보내기" ) - @PostMapping(value = "/chats/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> sendImageMessage(List chatImages){ return ResponseEntity.ok() .body(new SingleResponse<>(200, "채팅 사진 업로드 완료", chattingService.sendImageMessage(chatImages))); @@ -50,26 +51,9 @@ public ResponseEntity> sendImageMessage(List ch summary = "채팅 내용 리스트 가져오기", description = "Pageable에 해당하는 page, size, sort 내역에 맞게 반환" ) - @GetMapping("/chats/list/{chatRoomId}") - public ResponseEntity> getAllMessage(@PathVariable("chatRoomId") String id, - @RequestParam("page") int page){ - return ResponseEntity.ok().body(new SingleResponse<>(200, "채팅 내용 리스트 반환 완료", chattingService.getAllMessage(id, page))); - } - - //채팅 테스트를 위한 MVC - @GetMapping("/chat") - public String chatHtml(){ - return "chat"; - } - - @GetMapping("/chat/image") - public String chatImageHtml(){ - return "chatImage"; - } - - @GetMapping("/chat/room") - public String chatroomHtml(){ - return "chatroom"; + @GetMapping("/list/{chatRoomId}") + public ResponseEntity> getAllMessage(@PathVariable("chatRoomId") String chatRoomId, @RequestParam("page") int page){ + return ResponseEntity.ok() + .body(new SingleResponse<>(200, "채팅 내용 리스트 반환 완료", chattingService.getAllMessage(chatRoomId, page))); } - } diff --git a/src/main/java/inu/codin/codin/domain/chat/controller/ChattingMvcController.java b/src/main/java/inu/codin/codin/domain/chat/controller/ChattingMvcController.java new file mode 100644 index 00000000..a956742b --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/controller/ChattingMvcController.java @@ -0,0 +1,24 @@ +package inu.codin.codin.domain.chat.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ChattingMvcController { + + //채팅 테스트를 위한 MVC + @GetMapping("/chat") + public String chatHtml(){ + return "chat"; + } + + @GetMapping("/chat/image") + public String chatImageHtml(){ + return "chatImage"; + } + + @GetMapping("/chat/room") + public String chatroomHtml(){ + return "chatroom"; + } +} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/entity/Chatting.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/Chatting.java similarity index 89% rename from src/main/java/inu/codin/codin/domain/chat/chatting/entity/Chatting.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatting/Chatting.java index 03bc7323..2b34878f 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/entity/Chatting.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/Chatting.java @@ -1,8 +1,7 @@ -package inu.codin.codin.domain.chat.chatting.entity; +package inu.codin.codin.domain.chat.domain.chatting; import inu.codin.codin.common.dto.BaseTimeEntity; -import inu.codin.codin.domain.chat.chatting.dto.ContentType; -import inu.codin.codin.domain.chat.chatting.dto.request.ChattingRequestDto; +import inu.codin.codin.domain.chat.dto.chatting.request.ChattingRequestDto; import jakarta.validation.constraints.NotBlank; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/ContentType.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/ContentType.java similarity index 58% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/ContentType.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatting/ContentType.java index 836f69ce..5c1d58d5 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/ContentType.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/ContentType.java @@ -1,4 +1,4 @@ -package inu.codin.codin.domain.chat.chatting.dto; +package inu.codin.codin.domain.chat.domain.chatting; import lombok.Getter; diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/ChattingArrivedEvent.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingArrivedEvent.java similarity index 53% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/ChattingArrivedEvent.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingArrivedEvent.java index 352bbe03..a902c271 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/ChattingArrivedEvent.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingArrivedEvent.java @@ -1,6 +1,7 @@ -package inu.codin.codin.domain.chat.chatting.dto.event; +package inu.codin.codin.domain.chat.domain.chatting.event; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; import lombok.Getter; import org.springframework.context.ApplicationEvent; @@ -8,9 +9,11 @@ public class ChattingArrivedEvent extends ApplicationEvent { private final Chatting chatting; + private final ChatRoom chatRoom; - public ChattingArrivedEvent(Object source, Chatting chatting) { + public ChattingArrivedEvent(Object source, Chatting chatting, ChatRoom chatRoom) { super(source); this.chatting = chatting; + this.chatRoom = chatRoom; } } diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/ChattingNotificationEvent.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingNotificationEvent.java similarity index 78% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/ChattingNotificationEvent.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingNotificationEvent.java index ddbff691..f7cec02b 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/ChattingNotificationEvent.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingNotificationEvent.java @@ -1,6 +1,6 @@ -package inu.codin.codin.domain.chat.chatting.dto.event; +package inu.codin.codin.domain.chat.domain.chatting.event; -import inu.codin.codin.domain.chat.chatroom.entity.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; import lombok.Getter; import org.bson.types.ObjectId; import org.springframework.context.ApplicationEvent; diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/UpdateUnreadCountEvent.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingUnreadCountEvent.java similarity index 51% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/UpdateUnreadCountEvent.java rename to src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingUnreadCountEvent.java index ce80d951..02560d0d 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/event/UpdateUnreadCountEvent.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatting/event/ChattingUnreadCountEvent.java @@ -1,18 +1,18 @@ -package inu.codin.codin.domain.chat.chatting.dto.event; +package inu.codin.codin.domain.chat.domain.chatting.event; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; import lombok.Getter; import org.springframework.context.ApplicationEvent; import java.util.List; @Getter -public class UpdateUnreadCountEvent extends ApplicationEvent { +public class ChattingUnreadCountEvent extends ApplicationEvent { private final List chattingList; private final String chatRoomId; - public UpdateUnreadCountEvent(Object source, List chattingList, String chatRoomId) { + public ChattingUnreadCountEvent(Object source, List chattingList, String chatRoomId) { super(source); this.chattingList = chattingList; this.chatRoomId = chatRoomId; diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/request/ChattingRequestDto.java b/src/main/java/inu/codin/codin/domain/chat/dto/chatting/request/ChattingRequestDto.java similarity index 61% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/request/ChattingRequestDto.java rename to src/main/java/inu/codin/codin/domain/chat/dto/chatting/request/ChattingRequestDto.java index 390b434e..c560096e 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/request/ChattingRequestDto.java +++ b/src/main/java/inu/codin/codin/domain/chat/dto/chatting/request/ChattingRequestDto.java @@ -1,7 +1,6 @@ -package inu.codin.codin.domain.chat.chatting.dto.request; +package inu.codin.codin.domain.chat.dto.chatting.request; -import inu.codin.codin.domain.chat.chatting.dto.ContentType; -import inu.codin.codin.domain.chat.chatting.entity.MessageType; +import inu.codin.codin.domain.chat.domain.chatting.ContentType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -12,10 +11,6 @@ @Setter public class ChattingRequestDto { - @NotNull - @Schema(description = "STOMP 프로토콜 type", example = "SEND") - private MessageType type; - @NotBlank @Schema(description = "채팅 내용", example = "안녕하세요") private String content; diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/response/ChattingAndUserIdResponseDto.java b/src/main/java/inu/codin/codin/domain/chat/dto/chatting/response/ChattingAndUserIdResponseDto.java similarity index 67% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/response/ChattingAndUserIdResponseDto.java rename to src/main/java/inu/codin/codin/domain/chat/dto/chatting/response/ChattingAndUserIdResponseDto.java index 98c0aea0..47240ac3 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/response/ChattingAndUserIdResponseDto.java +++ b/src/main/java/inu/codin/codin/domain/chat/dto/chatting/response/ChattingAndUserIdResponseDto.java @@ -1,4 +1,4 @@ -package inu.codin.codin.domain.chat.chatting.dto.response; +package inu.codin.codin.domain.chat.dto.chatting.response; import lombok.Builder; import lombok.Getter; @@ -8,9 +8,9 @@ @Getter public class ChattingAndUserIdResponseDto { - private List chatting; + private final List chatting; - private String currentUserId; + private final String currentUserId; @Builder public ChattingAndUserIdResponseDto(List chatting, String currentUserId) { diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/response/ChattingResponseDto.java b/src/main/java/inu/codin/codin/domain/chat/dto/chatting/response/ChattingResponseDto.java similarity index 70% rename from src/main/java/inu/codin/codin/domain/chat/chatting/dto/response/ChattingResponseDto.java rename to src/main/java/inu/codin/codin/domain/chat/dto/chatting/response/ChattingResponseDto.java index 632918df..84a00335 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/dto/response/ChattingResponseDto.java +++ b/src/main/java/inu/codin/codin/domain/chat/dto/chatting/response/ChattingResponseDto.java @@ -1,8 +1,8 @@ -package inu.codin.codin.domain.chat.chatting.dto.response; +package inu.codin.codin.domain.chat.dto.chatting.response; import com.fasterxml.jackson.annotation.JsonFormat; -import inu.codin.codin.domain.chat.chatting.dto.ContentType; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; +import inu.codin.codin.domain.chat.domain.chatting.ContentType; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Builder; @@ -52,19 +52,11 @@ public ChattingResponseDto(String id, String senderId, String content, ContentTy this.unread = unread; } - public static ChattingResponseDto of(Chatting chatting){ - return ChattingResponseDto.builder() - .id(chatting.get_id().toString()) - .senderId(chatting.getSenderId().toString()) - .content(chatting.getContent()) - .createdAt(chatting.getCreatedAt()) - .contentType(chatting.getContentType()) - .chatRoomId(chatting.getChatRoomId().toString()) - .unread(chatting.getUnreadCount()) - .build(); + public static ChattingResponseDto of(Chatting chatting) { + return of(chatting, null); } - public static ChattingResponseDto of(Chatting chatting, ObjectId currentUserId){ + public static ChattingResponseDto of(Chatting chatting, ObjectId currentUserId) { return ChattingResponseDto.builder() .id(chatting.get_id().toString()) .senderId(chatting.getSenderId().toString()) @@ -72,8 +64,9 @@ public static ChattingResponseDto of(Chatting chatting, ObjectId currentUserId){ .createdAt(chatting.getCreatedAt()) .contentType(chatting.getContentType()) .chatRoomId(chatting.getChatRoomId().toString()) - .currentUserId(currentUserId.toString()) + .currentUserId(currentUserId != null ? currentUserId.toString() : null) .unread(chatting.getUnreadCount()) .build(); } + } diff --git a/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java b/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java new file mode 100644 index 00000000..0be6b478 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java @@ -0,0 +1,100 @@ +package inu.codin.codin.domain.chat.event; + +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.Participants; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; +import inu.codin.codin.domain.chat.domain.chatting.event.ChattingArrivedEvent; +import inu.codin.codin.domain.chat.domain.chatting.event.ChattingNotificationEvent; +import inu.codin.codin.domain.chat.domain.chatting.event.ChattingUnreadCountEvent; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import inu.codin.codin.domain.notification.service.NotificationService; +import inu.codin.codin.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class ChattingEventListener { + + private final ChatRoomRepository chatRoomRepository; + private final UserRepository userRepository; + private final SimpMessageSendingOperations template; + private final NotificationService notificationService; + + /** + 채팅을 발신했을 경우, + 1. 채팅방의 마지막 메세지 업데이트 + 2. 상대방이 접속한 상태가 아니라면 상대방의 unread 값 +1 + 3. /queue/chatroom/unread 를 통해 상대방의 채팅방 목록 실시간 업데이트 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleChattingArrivedEvent(ChattingArrivedEvent event){ + updateLastMessage(event); + updateUnreadCountAndNotify(event.getChatting(), event.getChatRoom()); + chatRoomRepository.save(event.getChatRoom()); + } + + private void updateLastMessage(ChattingArrivedEvent event){ + event.getChatRoom().updateLastMessage(event.getChatting().getContent()); + } + + private void updateUnreadCountAndNotify(Chatting chatting, ChatRoom chatRoom) { + Participants participants = chatRoom.getParticipants(); + participants.getDisconnectedUsersAndUpdateUnreadCount(chatting.getSenderId()) + .forEach(receiverInfo -> sendUnreadCountChange(receiverInfo.receiverId(), getLastMessageAndUnread(chatting, receiverInfo.unreadMessage()))); + } + + private Map getLastMessageAndUnread(Chatting chatting, int unreadMessage) { + return Map.of( + "chatRoomId", chatting.getChatRoomId().toString(), + "lastMessage", chatting.getContent(), + "unread", String.valueOf(unreadMessage) + ); + } + + private void sendUnreadCountChange(ObjectId receiverId, Map result) { + userRepository.findByUserId(receiverId) + .ifPresent(userEntity -> template.convertAndSendToUser(userEntity.getEmail(), "/queue/chatroom/unread", result)); + } + + /** + * 채팅을 받는 사람들에게 알림 보내기 + * @param event + */ + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleChattingNotificationEvent(ChattingNotificationEvent event){ + ChatRoom chatRoom = event.getChatRoom(); + chatRoom.getParticipants().getUsersToNotify(event.getUserId()) + .forEach(userId -> notificationService.sendNotificationMessageByChat(userId, chatRoom.get_id())); + } + + /** + 유저가 채팅방 입장 시, 읽지 않은 채팅에 대하여 새로운 unread 값 송신 + 클라이언트 : chat_id 와 일치하는 채팅값의 unread 값 업데이트 + */ + @EventListener + public void updateUnreadCountEvent(ChattingUnreadCountEvent chattingUnreadCountEvent){ + List> unreadChattingList = + chattingUnreadCountEvent.getChattingList().stream() + .map(chatting -> Map.of( + "id", chatting.get_id().toString(), + "unread", String.valueOf(chatting.getUnreadCount()) + )).toList(); + + template.convertAndSend("/queue/unread/"+ chattingUnreadCountEvent.getChatRoomId(), unreadChattingList); + } + +} diff --git a/src/main/java/inu/codin/codin/domain/chat/chatting/repository/ChattingRepository.java b/src/main/java/inu/codin/codin/domain/chat/repository/ChattingRepository.java similarity index 78% rename from src/main/java/inu/codin/codin/domain/chat/chatting/repository/ChattingRepository.java rename to src/main/java/inu/codin/codin/domain/chat/repository/ChattingRepository.java index 2f9c21ab..93ecac02 100644 --- a/src/main/java/inu/codin/codin/domain/chat/chatting/repository/ChattingRepository.java +++ b/src/main/java/inu/codin/codin/domain/chat/repository/ChattingRepository.java @@ -1,6 +1,6 @@ -package inu.codin.codin.domain.chat.chatting.repository; +package inu.codin.codin.domain.chat.repository; -import inu.codin.codin.domain.chat.chatting.entity.Chatting; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; import org.bson.types.ObjectId; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; @@ -10,7 +10,7 @@ public interface ChattingRepository extends MongoRepository { - List findAllByChatRoomIdOrderByCreatedAtDesc(ObjectId chatRoomId); + List findAllByChatRoomIdOrderByCreatedAtDesc(ObjectId chatRoomId, Pageable pageable); List findAllByChatRoomId(ObjectId id, Pageable pageable); diff --git a/src/main/java/inu/codin/codin/domain/chat/service/ChattingService.java b/src/main/java/inu/codin/codin/domain/chat/service/ChattingService.java new file mode 100644 index 00000000..6db2f1fa --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/service/ChattingService.java @@ -0,0 +1,113 @@ +package inu.codin.codin.domain.chat.service; + +import inu.codin.codin.common.security.util.SecurityUtils; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.ParticipantInfo; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; +import inu.codin.codin.domain.chat.domain.chatting.event.ChattingArrivedEvent; +import inu.codin.codin.domain.chat.domain.chatting.event.ChattingNotificationEvent; +import inu.codin.codin.domain.chat.dto.chatting.request.ChattingRequestDto; +import inu.codin.codin.domain.chat.dto.chatting.response.ChattingAndUserIdResponseDto; +import inu.codin.codin.domain.chat.dto.chatting.response.ChattingResponseDto; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import inu.codin.codin.domain.chat.repository.ChattingRepository; +import inu.codin.codin.domain.user.security.CustomUserDetails; +import inu.codin.codin.infra.s3.S3Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Stream; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChattingService { + + private final ChatRoomRepository chatRoomRepository; + + private final ChatRoomService chatRoomService; + private final ChattingRepository chattingRepository; + private final S3Service s3Service; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public ChattingResponseDto sendMessage(String chatRoomId, ChattingRequestDto chattingRequestDto, Authentication authentication) { + ChatRoom chatRoom = chatRoomService.getChatRoom(chatRoomId); + ObjectId userId = ((CustomUserDetails) authentication.getPrincipal()).getId(); + + //상대가 채팅방을 나간 상태라면 다시 불러와서 채팅 시작 + reactivateChatRoomForUser(chatRoom, userId); + + int unreadCount = getUnreadCount(chatRoom); + Chatting chatting = Chatting.of(chatRoom.get_id(), chattingRequestDto, userId, unreadCount); + chattingRepository.save(chatting); + + log.info("[메시지 전송 성공] 메시지: [{}], 송신자 ID: {}, 채팅방 ID: {}", chattingRequestDto.getContent(), userId, chatRoomId); + + publishUnreadCountAndNotify(chatting, chatRoom, userId); + return ChattingResponseDto.of(chatting); + } + + private void publishUnreadCountAndNotify(Chatting chatting, ChatRoom chatRoom, ObjectId userId) { + //상대 유저가 접속하지 않은 상태라면 unread 개수 업데이트 및 마지막 대화 내용 업데이트 + eventPublisher.publishEvent(new ChattingArrivedEvent(this, chatting, chatRoom)); + //알림 보내기 + eventPublisher.publishEvent(new ChattingNotificationEvent(this, userId, chatRoom)); + } + + public ChattingAndUserIdResponseDto getAllMessage(String chatRoomId, int page) { + ObjectId userId = SecurityUtils.getCurrentUserId(); + ChatRoom chatRoom = chatRoomService.getChatRoom(chatRoomId); + + Pageable pageable = PageRequest.of(page, 20, Sort.by("createdAt").descending()); + List chattingResponseDto = getChattingSinceRejoin(chatRoom.get_id(), chatRoom, userId, pageable); + log.info("[메시지 조회 성공] 채팅방 ID: {}, 메시지 개수: {}", chatRoomId, chattingResponseDto.size()); + + return new ChattingAndUserIdResponseDto(chattingResponseDto, userId.toString()); + } + + private List getChattingSinceRejoin(ObjectId chatRoomId, ChatRoom chatRoom, ObjectId userId, Pageable pageable) { + LocalDateTime whenLeaved = chatRoom.getParticipants().getWhenLeaved(userId); + return getChattingStream(chatRoomId, whenLeaved, pageable) + .map(ChattingResponseDto::of) + .toList(); + } + + private Stream getChattingStream(ObjectId chatRoomId, LocalDateTime whenLeaved, Pageable pageable) { + if (whenLeaved != null) + return chattingRepository.findAllByChatRoomIdAndCreatedAtAfter(chatRoomId, whenLeaved, pageable).stream(); + else + return chattingRepository.findAllByChatRoomId(chatRoomId, pageable).stream(); + } + + public List sendImageMessage(List chatImages) { + log.info("[이미지 메시지 전송] 이미지 개수: {}", chatImages.size()); + List imageUrls = s3Service.handleImageUpload(chatImages); + log.info("[이미지 메시지 전송 성공] 업로드된 이미지 URL 개수: {}", imageUrls.size()); + return imageUrls; + } + + private void reactivateChatRoomForUser(ChatRoom chatRoom, ObjectId userId) { + List participantInfos = chatRoom.getParticipants().remainReceiver(userId); + if (!participantInfos.isEmpty()){ + chatRoomRepository.save(chatRoom); + } + } + + private int getUnreadCount(ChatRoom chatRoom) { + //채팅방에 접속해 있는 사람 수 빼기 (읽은 count) + int countOfParticipating = chatRoom.getParticipants().getCountOfConnecting(); + return chatRoom.getParticipants().size() - countOfParticipating; + } +} diff --git a/src/main/java/inu/codin/codin/domain/chat/stomp/HttpHandShakeInterceptor.java b/src/main/java/inu/codin/codin/domain/chat/stomp/HttpHandShakeInterceptor.java new file mode 100644 index 00000000..4663e048 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/stomp/HttpHandShakeInterceptor.java @@ -0,0 +1,47 @@ +package inu.codin.codin.domain.chat.stomp; + +import inu.codin.codin.common.security.exception.JwtException; +import inu.codin.codin.common.security.exception.SecurityErrorCode; +import inu.codin.codin.common.security.service.JwtService; +import io.jsonwebtoken.MalformedJwtException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +@RequiredArgsConstructor +public class HttpHandShakeInterceptor implements HandshakeInterceptor { + + private final JwtService jwtService; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { + if (request instanceof ServletServerHttpRequest){ + ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; + + try { + jwtService.setAuthentication(serverHttpRequest); + } catch (MessageDeliveryException e) { + throw new MessageDeliveryException("[Chatting] Jwt로 인한 메세지 전송 오류입니다."); + } catch (MalformedJwtException e) { + throw new MalformedJwtException("[Chatting] 비정상적인 jwt 토큰 입니다."); + } catch (Exception e) { + throw new JwtException(SecurityErrorCode.INVALID_TOKEN, "[Chatting] 인증되지 않은 jwt 토큰 입니다."); + } + } + + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + + } + + +} diff --git a/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageProcessor.java b/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageProcessor.java new file mode 100644 index 00000000..d5e6ca1b --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageProcessor.java @@ -0,0 +1,48 @@ +package inu.codin.codin.domain.chat.stomp; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StompMessageProcessor implements ChannelInterceptor { + + private final StompMessageService stompMessageService; + + @Override + public Message preSend(Message message, MessageChannel messageChannel){ + StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + handleMessage(headerAccessor); + return message; + } + + public void handleMessage(StompHeaderAccessor headerAccessor){ + if (headerAccessor == null || headerAccessor.getCommand() == null){ + throw new MessageDeliveryException(HttpStatus.BAD_REQUEST.toString()); + } + + switch (headerAccessor.getCommand()) { + case CONNECT -> stompMessageService.connectSession(headerAccessor); + case SUBSCRIBE -> { + if (Objects.requireNonNull(headerAccessor.getDestination()).matches("/queue(/unread)?/[^/]+")) + //헤더에 chatRoomId가 필요한 destination + stompMessageService.enterToChatRoom(headerAccessor); + } + case UNSUBSCRIBE -> stompMessageService.exitToChatRoom(headerAccessor); //채팅방에서 끊긴 상태 + case DISCONNECT -> stompMessageService.disconnectSession(headerAccessor); //stomp 세션 끊기 + } + } + + +} diff --git a/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java b/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java new file mode 100644 index 00000000..8fa93a75 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java @@ -0,0 +1,144 @@ +package inu.codin.codin.domain.chat.stomp; + +import inu.codin.codin.common.exception.NotFoundException; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatting.Chatting; +import inu.codin.codin.domain.chat.domain.chatting.event.ChattingUnreadCountEvent; +import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; +import inu.codin.codin.domain.chat.exception.ChatRoomException; +import inu.codin.codin.domain.chat.exception.ChattingErrorCode; +import inu.codin.codin.domain.chat.exception.ChattingException; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import inu.codin.codin.domain.chat.repository.ChattingRepository; +import inu.codin.codin.domain.user.entity.UserEntity; +import inu.codin.codin.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.mongodb.core.BulkOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Service +@RequiredArgsConstructor +@Slf4j +public class StompMessageService { + + private final Map sessionStore = new ConcurrentHashMap<>(); + + private final ChatRoomRepository chatRoomRepository; + private final UserRepository userRepository; + private final ChattingRepository chattingRepository; + + private final MongoTemplate mongoTemplate; + private final ApplicationEventPublisher eventPublisher; + + public void connectSession(StompHeaderAccessor headerAccessor) { + String sessionId = headerAccessor.getSessionId(); + sessionStore.put(sessionId, ""); + log.info("[STOMP CONNECT] session 연결 : {}", sessionId); + } + + @Transactional + public void enterToChatRoom(StompHeaderAccessor headerAccessor){ + ChatRoomContext context = resolveChatRoomContext(headerAccessor); + + sessionStore.put(headerAccessor.getSessionId(), context.chatroom().get_id().toString()); + + context.chatroom.getParticipants().enter(context.user.get_id()); + chatRoomRepository.save(context.chatroom); + log.info("[STOMP SUBSCRIBE] session : {}, chatRoomId : {} ", headerAccessor.getSessionId(), context.chatroom().get_id().toString()); + + publishingUnreadCount(context); + } + + private void publishingUnreadCount(ChatRoomContext context) { + List updateChats = updateUnreadCount(context.chatroom.get_id(), context.user.get_id()); + if (!updateChats.isEmpty()) + eventPublisher.publishEvent(new ChattingUnreadCountEvent(this, updateChats, context.chatroom.get_id().toString())); + } + + @Transactional + public void exitToChatRoom(StompHeaderAccessor headerAccessor) { + ChatRoomContext context = resolveChatRoomContext(headerAccessor); + + context.chatroom().getParticipants().exit(context.user().get_id()); + chatRoomRepository.save(context.chatroom()); + log.info("[STOMP UNSUBSCRIBE] session : {}, chatRoomId : {} ", headerAccessor.getSessionId(), context.chatroom().get_id().toString()); + } + + public void disconnectSession(StompHeaderAccessor headerAccessor){ + sessionStore.remove(headerAccessor.getSessionId()); + log.info("[STOMP DISCONNECT] session : {} ", headerAccessor.getSessionId()); + } + + private ChatRoomContext resolveChatRoomContext(StompHeaderAccessor headerAccessor) { + UserEntity user = getUserEntityOrThrow(headerAccessor); + ChatRoom chatroom = getChatRoomOrThrow(headerAccessor); + + return new ChatRoomContext(chatroom, user); + } + + private UserEntity getUserEntityOrThrow(StompHeaderAccessor headerAccessor) { + String email = Optional.ofNullable(headerAccessor.getUser()) + .map(Principal::getName) + .orElseThrow(() -> new ChattingException(ChattingErrorCode.CHATTING_USER_NOT_FOUND, headerAccessor.getSessionId())); + + return userRepository.findByEmailAndStatusAll(email) + .orElseThrow(()-> new NotFoundException("유저를 찾을 수 없습니다.")); + } + + private ChatRoom getChatRoomOrThrow(StompHeaderAccessor headerAccessor) { + String chatroomId = headerAccessor.getFirstNativeHeader("chatRoomId"); + if (chatroomId == null || !ObjectId.isValid(chatroomId)) + throw new ChattingException(ChattingErrorCode.CHATTING_ID_NOT_FOUND, headerAccessor.getSessionId()); + + return getChatRoom(new ObjectId(chatroomId)); + } + + private ChatRoom getChatRoom(ObjectId chatroomId) { + return chatRoomRepository.findBy_idAndDeletedAtIsNull(chatroomId) + .orElseThrow(() -> new ChatRoomException(ChatRoomErrorCode.CHATROOM_NOT_FOUND)); + } + + private record ChatRoomContext(ChatRoom chatroom, UserEntity user) { + } + + private List updateUnreadCount(ObjectId chatRoomId, ObjectId userId){ + ChatRoom chatRoom = getChatRoom(chatRoomId); + int unreadCount = chatRoom.getParticipants().getUnreadCount(userId); + + //유저가 읽지 않은 채팅만 가져와서 채팅의 unreadCount를 줄인다. + List unreadChats = chattingRepository.findAllByChatRoomIdOrderByCreatedAtDesc(chatRoomId, PageRequest.of(0, unreadCount)); + if (!unreadChats.isEmpty()) + bulkUpdateUnreadCount(unreadChats); + return unreadChats; + } + + private void bulkUpdateUnreadCount(List chats) { + BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, Chatting.class); + + chats.forEach(chat -> { + Query query = new Query(where("_id").is(chat.get_id())); + Update update = new Update().inc("unreadCount", -1); + bulkOps.updateOne(query, update); + }); + + bulkOps.execute(); + } + +} From 15910b43e604a2478f6ecd8fd0441588d4e37eb0 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Sat, 28 Jun 2025 00:17:30 +0900 Subject: [PATCH 03/15] =?UTF-8?q?refactor=20:=20SecurityUtils=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EB=A1=9C=EC=A7=81=20=EB=AA=A8=EB=93=88=ED=99=94=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/security/util/SecurityUtils.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java b/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java index 0f11558e..d98c07d7 100644 --- a/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java +++ b/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java @@ -20,27 +20,37 @@ public class SecurityUtils { * @throws JwtException 인증 정보가 없는 경우 예외 발생 */ public static ObjectId getCurrentUserId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !(authentication.getPrincipal() instanceof CustomUserDetails userDetails)) { - throw new JwtException(SecurityErrorCode.ACCESS_DENIED); - } - + CustomUserDetails userDetails = getCustomUserDetails(); return userDetails.getId(); } + /** + * 현재 인증된 사용자의 ROLE를 반환. + * + * @return 인증된 사용자의 ROLE + * @throws JwtException 인증 정보가 없는 경우 예외 발생 + */ public static UserRole getCurrentUserRole(){ + CustomUserDetails userDetails = getCustomUserDetails(); + return userDetails.getRole(); + } + + private static CustomUserDetails getCustomUserDetails() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.getPrincipal() instanceof CustomUserDetails userDetails)) { throw new JwtException(SecurityErrorCode.ACCESS_DENIED); } - - return userDetails.getRole(); + return userDetails; } + /** + * 매개변수 id가 현재 로그인 한 유저의 _Id와 일치하는지 확인 + * @param id + * @throws JwtException 일치하지 않을 경우 에러 발생 + */ public static void validateUser(ObjectId id){ - ObjectId userId = SecurityUtils.getCurrentUserId(); + ObjectId userId = getCurrentUserId(); if (!id.equals(userId)) { throw new JwtException(SecurityErrorCode.ACCESS_DENIED, "현재 유저에게 권한이 없습니다."); } From 44c42a25816793da00adb483861db56cb89c10f2 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Tue, 1 Jul 2025 00:00:08 +0900 Subject: [PATCH 04/15] =?UTF-8?q?test=20:=20Chat=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatRoomServiceTest.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java diff --git a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java new file mode 100644 index 00000000..5d7915dd --- /dev/null +++ b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java @@ -0,0 +1,83 @@ +package inu.codin.codin.domain.chat.service; + +import inu.codin.codin.common.security.util.SecurityUtils; +import inu.codin.codin.domain.block.service.BlockService; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.event.ChatRoomNotificationEvent; +import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; +import inu.codin.codin.domain.chat.dto.chatroom.response.ChatRoomCreateResponseDto; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import inu.codin.codin.domain.user.security.CustomUserDetails; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ChatRoomServiceTest { + + @InjectMocks + ChatRoomService chatRoomService; + @Mock + ChatRoomRepository chatRoomRepository; + @Mock + SecurityUtils securityUtils; + @Mock + ChatRoomValidator chatRoomValidator; + @Mock + BlockService blockService; + @Mock + ApplicationEventPublisher eventPublisher; + + @BeforeEach + public void setCustomDetails(){ + CustomUserDetails userDetails = CustomUserDetails.builder().email("test@test.com").build(); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @Test + @DisplayName("채팅방을 생성합니다.") + public void 채팅방_생성(){ + //given + ObjectId senderId = new ObjectId(); + given(SecurityUtils.getCurrentUserId()).willReturn(senderId); + + ChatRoomCreateRequestDto requestDto = ChatRoomCreateRequestDto.builder() + .roomName("roomName") + .receiverId("receiverId") + .referenceId("referenceId") + .build(); + ChatRoom chatRoom = ChatRoom.of(requestDto, senderId); + given(chatRoomRepository.save(any(ChatRoom.class))).willReturn(chatRoom); + + //when + ChatRoomCreateResponseDto createResponseDto = chatRoomService.createChatRoom(requestDto); + + //then + verify(chatRoomRepository, times(1)).save(any(ChatRoom.class)); + verify(eventPublisher, times(1)).publishEvent(any(ChatRoomNotificationEvent.class)); + assertThat(createResponseDto.getChatRoomId()).isEqualTo(chatRoom.get_id().toString()); + } + + @Test + @DisplayName("채팅방 생성 중 오류 발생 - Receiver가 존재하는지 확인 NotFoundException") + public void 채팅방_오류_NotFoundException(){ + + } + +} \ No newline at end of file From e6d43a92061a1f151b4900259d822891d7e9200f Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 00:40:24 +0900 Subject: [PATCH 05/15] =?UTF-8?q?fix=20:=20=EC=A4=91=EB=B3=B5=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codin/common/config/WebSocketConfig.java | 55 ------------------- .../stomp/HttpHandShakeInterceptor.java | 47 ---------------- .../common/stomp/StompMessageProcessor.java | 48 ---------------- 3 files changed, 150 deletions(-) delete mode 100644 src/main/java/inu/codin/codin/common/config/WebSocketConfig.java delete mode 100644 src/main/java/inu/codin/codin/common/stomp/HttpHandShakeInterceptor.java delete mode 100644 src/main/java/inu/codin/codin/common/stomp/StompMessageProcessor.java diff --git a/src/main/java/inu/codin/codin/common/config/WebSocketConfig.java b/src/main/java/inu/codin/codin/common/config/WebSocketConfig.java deleted file mode 100644 index ed6205a6..00000000 --- a/src/main/java/inu/codin/codin/common/config/WebSocketConfig.java +++ /dev/null @@ -1,55 +0,0 @@ -package inu.codin.codin.common.config; - -import inu.codin.codin.common.security.service.JwtService; -import inu.codin.codin.common.stomp.HttpHandShakeInterceptor; -import inu.codin.codin.common.stomp.StompMessageProcessor; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.simp.config.ChannelRegistration; -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; -import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; - -@Configuration -@RequiredArgsConstructor -@EnableWebSocketMessageBroker -public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Value("${server.domain}") - private String BASEURL; - - private final StompMessageProcessor stompMessageProcessor; - private final JwtService jwtService; - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws-stomp") //handshake endpoint - .setAllowedOriginPatterns("http://localhost:3000", "http://localhost:8080", BASEURL) - .withSockJS() - .setInterceptors(new HttpHandShakeInterceptor(jwtService)); - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/topic", "/queue"); - //해당 주소를 구독 및 구독하고 있는 클라이언트들에게 메세지 전달 - //메세지를 브로커로 라우팅 - registry.setApplicationDestinationPrefixes("/pub"); - //클라이언트에서 보낸 메세지를 받을 prefix, controller의 @MessageMapping과 이어짐 - registry.setUserDestinationPrefix("/user"); - //convertAndSendToUser 사용할 prefix - } - - @Override - public void configureWebSocketTransport(WebSocketTransportRegistration registration) { - registration.setMessageSizeLimit(50 * 1024 * 1024); // 메세지 크기 제한 오류 방지(이 코드가 없으면 byte code를 보낼때 소켓 연결이 끊길 수 있음) - } - - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(stompMessageProcessor); - } -} diff --git a/src/main/java/inu/codin/codin/common/stomp/HttpHandShakeInterceptor.java b/src/main/java/inu/codin/codin/common/stomp/HttpHandShakeInterceptor.java deleted file mode 100644 index 20397ac7..00000000 --- a/src/main/java/inu/codin/codin/common/stomp/HttpHandShakeInterceptor.java +++ /dev/null @@ -1,47 +0,0 @@ -package inu.codin.codin.common.stomp; - -import inu.codin.codin.common.security.exception.JwtException; -import inu.codin.codin.common.security.exception.SecurityErrorCode; -import inu.codin.codin.common.security.service.JwtService; -import io.jsonwebtoken.MalformedJwtException; -import lombok.RequiredArgsConstructor; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.messaging.MessageDeliveryException; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.server.HandshakeInterceptor; - -import java.util.Map; - -@RequiredArgsConstructor -public class HttpHandShakeInterceptor implements HandshakeInterceptor { - - private final JwtService jwtService; - - @Override - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { - if (request instanceof ServletServerHttpRequest){ - ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; - - try { - jwtService.setAuthentication(serverHttpRequest); - } catch (MessageDeliveryException e) { - throw new MessageDeliveryException("[Chatting] Jwt로 인한 메세지 전송 오류입니다."); - } catch (MalformedJwtException e) { - throw new MalformedJwtException("[Chatting] 비정상적인 jwt 토큰 입니다."); - } catch (Exception e) { - throw new JwtException(SecurityErrorCode.INVALID_TOKEN, "[Chatting] 인증되지 않은 jwt 토큰 입니다."); - } - } - - return true; - } - - @Override - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { - - } - - -} diff --git a/src/main/java/inu/codin/codin/common/stomp/StompMessageProcessor.java b/src/main/java/inu/codin/codin/common/stomp/StompMessageProcessor.java deleted file mode 100644 index 043dd443..00000000 --- a/src/main/java/inu/codin/codin/common/stomp/StompMessageProcessor.java +++ /dev/null @@ -1,48 +0,0 @@ -package inu.codin.codin.common.stomp; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageDeliveryException; -import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.MessageHeaderAccessor; -import org.springframework.stereotype.Component; - -import java.util.Objects; - -@Component -@RequiredArgsConstructor -@Slf4j -public class StompMessageProcessor implements ChannelInterceptor { - - private final StompMessageService stompMessageService; - - @Override - public Message preSend(Message message, MessageChannel messageChannel){ - StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); - handleMessage(headerAccessor); - return message; - } - - public void handleMessage(StompHeaderAccessor headerAccessor){ - if (headerAccessor == null || headerAccessor.getCommand() == null){ - throw new MessageDeliveryException(HttpStatus.BAD_REQUEST.toString()); - } - - switch (headerAccessor.getCommand()) { - case CONNECT -> stompMessageService.connectSession(headerAccessor); - case SUBSCRIBE -> { - if (Objects.requireNonNull(headerAccessor.getDestination()).matches("/queue(/unread)?/[^/]+")) - //헤더에 chatRoomId가 필요한 destination - stompMessageService.enterToChatRoom(headerAccessor); - } - case UNSUBSCRIBE -> stompMessageService.exitToChatRoom(headerAccessor); //채팅방에서 끊긴 상태 - case DISCONNECT -> stompMessageService.disconnectSession(headerAccessor); //stomp 세션 끊기 - } - } - - -} From 62c1a834c9c6af76a8ca3623607370112ac759c3 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 00:40:43 +0900 Subject: [PATCH 06/15] =?UTF-8?q?refactor=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B8=EB=9D=BC=EC=9D=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inu/codin/codin/common/security/util/SecurityUtils.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java b/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java index d98c07d7..674b997a 100644 --- a/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java +++ b/src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java @@ -20,8 +20,7 @@ public class SecurityUtils { * @throws JwtException 인증 정보가 없는 경우 예외 발생 */ public static ObjectId getCurrentUserId() { - CustomUserDetails userDetails = getCustomUserDetails(); - return userDetails.getId(); + return getCustomUserDetails().getId(); } /** @@ -31,8 +30,7 @@ public static ObjectId getCurrentUserId() { * @throws JwtException 인증 정보가 없는 경우 예외 발생 */ public static UserRole getCurrentUserRole(){ - CustomUserDetails userDetails = getCustomUserDetails(); - return userDetails.getRole(); + return getCustomUserDetails().getRole(); } private static CustomUserDetails getCustomUserDetails() { From 9ba767562bde8f70a9858bb676887597528f2745 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 00:42:23 +0900 Subject: [PATCH 07/15] =?UTF-8?q?fix=20:=20unreadCount=20=EA=B0=92?= =?UTF-8?q?=EC=9D=B4=200=EB=B3=B4=EB=8B=A4=20=ED=81=B0=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EC=97=90=EB=A7=8C=20=ED=99=95=EC=9D=B8,=20bulk=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=83=81=EC=9D=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=8F=84=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/stomp/StompMessageService.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java b/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java index 8fa93a75..a3043051 100644 --- a/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java +++ b/src/main/java/inu/codin/codin/domain/chat/stomp/StompMessageService.java @@ -56,14 +56,14 @@ public void connectSession(StompHeaderAccessor headerAccessor) { @Transactional public void enterToChatRoom(StompHeaderAccessor headerAccessor){ ChatRoomContext context = resolveChatRoomContext(headerAccessor); - sessionStore.put(headerAccessor.getSessionId(), context.chatroom().get_id().toString()); + publishingUnreadCount(context); + context.chatroom.getParticipants().enter(context.user.get_id()); chatRoomRepository.save(context.chatroom); log.info("[STOMP SUBSCRIBE] session : {}, chatRoomId : {} ", headerAccessor.getSessionId(), context.chatroom().get_id().toString()); - publishingUnreadCount(context); } private void publishingUnreadCount(ChatRoomContext context) { @@ -122,10 +122,12 @@ private List updateUnreadCount(ObjectId chatRoomId, ObjectId userId){ ChatRoom chatRoom = getChatRoom(chatRoomId); int unreadCount = chatRoom.getParticipants().getUnreadCount(userId); - //유저가 읽지 않은 채팅만 가져와서 채팅의 unreadCount를 줄인다. - List unreadChats = chattingRepository.findAllByChatRoomIdOrderByCreatedAtDesc(chatRoomId, PageRequest.of(0, unreadCount)); - if (!unreadChats.isEmpty()) + List unreadChats = List.of(); + if (unreadCount > 0) { //unreadCount가 있을 경우, + //유저가 읽지 않은 채팅만 가져와서 채팅의 unreadCount를 줄인다. + unreadChats = chattingRepository.findAllByChatRoomIdOrderByCreatedAtDesc(chatRoomId, PageRequest.of(0, unreadCount)); bulkUpdateUnreadCount(unreadChats); + } return unreadChats; } @@ -136,6 +138,8 @@ private void bulkUpdateUnreadCount(List chats) { Query query = new Query(where("_id").is(chat.get_id())); Update update = new Update().inc("unreadCount", -1); bulkOps.updateOne(query, update); + + chat.minusUnread(); }); bulkOps.execute(); From ceb97353b9d3279894f0aefc9a90f872571a0cb8 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 01:06:19 +0900 Subject: [PATCH 08/15] =?UTF-8?q?refactor=20:=20=EB=A7=A4=EA=B0=9C?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codin/domain/chat/controller/ChattingController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java b/src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java index 34439fc6..7f33dab0 100644 --- a/src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java +++ b/src/main/java/inu/codin/codin/domain/chat/controller/ChattingController.java @@ -32,8 +32,9 @@ public class ChattingController { ) @MessageMapping("/chats/{chatRoomId}") //client 측에서 앞에 '/pub' 를 붙여서 요청 @SendTo("/queue/{chatRoomId}") - public ResponseEntity> sendMessage(@DestinationVariable("chatRoomId") String chatRoomId, @RequestBody @Valid ChattingRequestDto chattingRequestDto, - @AuthenticationPrincipal Authentication authentication){ + public ResponseEntity> sendMessage(@DestinationVariable("chatRoomId") String chatRoomId, + @RequestBody @Valid ChattingRequestDto chattingRequestDto, + @AuthenticationPrincipal Authentication authentication){ return ResponseEntity.ok() .body(new SingleResponse<>(200, "채팅 송신 완료", chattingService.sendMessage(chatRoomId, chattingRequestDto, authentication))); } From 1b23db72703c1badb9d340ea998eac1b09d163c3 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 17:32:30 +0900 Subject: [PATCH 09/15] =?UTF-8?q?test=20:=20ChatRoomValidator.java=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatRoomValidator.java | 2 +- .../chat/service/ChatRoomValidatorTest.java | 137 ++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java diff --git a/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java index 5cff9f40..31b4ae32 100644 --- a/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java +++ b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java @@ -32,7 +32,7 @@ public void validate(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId private void validateNotSelfChat(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { if (senderId.equals(chatRoomCreateRequestDto.getReceiverId())) - throw new ChatRoomException(ChatRoomErrorCode.CHATROOM_EXISTED); + throw new ChatRoomException(ChatRoomErrorCode.CHATROOM_CREATE_MYSELF); } private void validateChatRoomDuplicated(ChatRoomCreateRequestDto chatRoomCreateRequestDto, ObjectId senderId) { diff --git a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java new file mode 100644 index 00000000..f7d36c4a --- /dev/null +++ b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java @@ -0,0 +1,137 @@ +package inu.codin.codin.domain.chat.service; + +import inu.codin.codin.common.exception.NotFoundException; +import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.Participants; +import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; +import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; +import inu.codin.codin.domain.chat.exception.ChatRoomException; +import inu.codin.codin.domain.chat.exception.ChatRoomExistedException; +import inu.codin.codin.domain.chat.repository.ChatRoomRepository; +import inu.codin.codin.domain.user.entity.UserEntity; +import inu.codin.codin.domain.user.repository.UserRepository; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ChatRoomValidatorTest { + + @InjectMocks + ChatRoomValidator chatRoomValidator; + + @Mock + ChatRoomRepository chatRoomRepository; + + @Mock + UserRepository userRepository; + + ObjectId senderId = new ObjectId(); + ChatRoomCreateRequestDto requestDto = ChatRoomCreateRequestDto.builder() + .referenceId(new ObjectId().toString()) + .receiverId(senderId.toString()).build(); + + @Test + @DisplayName("송신자와 수신자가 같을 경우 에러 반환") + void 송신자_수신자_동일(){ + //given + given(userRepository.findById(senderId)).willReturn(Optional.ofNullable(mock(UserEntity.class))); + given(chatRoomRepository.findByReferenceIdAndParticipantsContaining(any(), any(), any())).willReturn(Optional.empty()); + + //when & then + assertThatThrownBy(() -> chatRoomValidator.validate(requestDto, senderId)) + .isInstanceOf(ChatRoomException.class) + .hasMessage(ChatRoomErrorCode.CHATROOM_CREATE_MYSELF.message()); + } + + @Test + @DisplayName("채팅방이 이미 존재하는 경우 에러 반환") + void 채팅방_이미_존재(){ + //given + //참여자 생성 + ChatRoom chatroom = getChatRoom(); + + given(userRepository.findById(senderId)).willReturn(Optional.ofNullable(mock(UserEntity.class))); + given(chatRoomRepository.findByReferenceIdAndParticipantsContaining(any(), any(), any())).willReturn(Optional.of(chatroom)); + + //when & then + assertThatThrownBy(() -> chatRoomValidator.validate(requestDto, senderId)) + .isInstanceOf(ChatRoomExistedException.class) + .hasMessage(ChatRoomErrorCode.CHATROOM_EXISTED.message()); + + verify(chatRoomRepository, never()).save(any()); + } + + @Test + @DisplayName("채팅방이 이미 존재하는 경우, 상대방이 떠난 상태라면 다시 불러온 후 에러 반환") + void 채팅방_이미_존재_상대방_불러오기(){ + //given + //참여자 생성 + ChatRoom chatroom = getChatRoomWithLeaveParticipants(); + + given(userRepository.findById(senderId)).willReturn(Optional.ofNullable(mock(UserEntity.class))); + given(chatRoomRepository.findByReferenceIdAndParticipantsContaining(any(), any(), any())).willReturn(Optional.of(chatroom)); + + //when & then + assertThatThrownBy(() -> chatRoomValidator.validate(requestDto, senderId)) + .isInstanceOf(ChatRoomExistedException.class) + .hasMessage(ChatRoomErrorCode.CHATROOM_EXISTED.message()); + + //채팅방에 다시 불러왔기 때문에 leave 상태가 false가 되었음 + assertFalse(chatroom.getParticipants().getInfo().get(senderId).isLeaved()); + verify(chatRoomRepository, times(1)).save(any()); + } + + @Test + @DisplayName("Receiver가 존재하지 않을 경우 에러 반환") + void RECEIVER_존재하지_않음(){ + //given + String errorMessage = "Receive 유저를 찾을 수 없습니다."; + doThrow(new NotFoundException(errorMessage)) + .when(userRepository).findById(senderId); + + //when & then + assertThatThrownBy(() -> chatRoomValidator.validate(requestDto, senderId)) + .isInstanceOf(NotFoundException.class) + .hasMessage(errorMessage); + } + + @Test + @DisplayName("채팅방의 참여자가 모두 떠났을 경우 채팅방 삭제") + void 채팅방_참여자가_없을경우_삭제(){ + //given + ChatRoom chatRoom = getChatRoomWithLeaveParticipants(); + + //when + chatRoomValidator.validateParticipantsAllLeaved(new ObjectId().toString(), chatRoom); + + //then + assertNotNull(chatRoom.getDeletedAt()); + verify(chatRoomRepository, times(1)).save(any()); + } + + private ChatRoom getChatRoom() { + Participants participants = new Participants(); + participants.create(senderId); + return ChatRoom.builder().participants(participants).build(); + } + + private ChatRoom getChatRoomWithLeaveParticipants() { + Participants participants = new Participants(); + participants.create(senderId); + participants.getInfo().get(senderId).leave(); + return ChatRoom.builder().participants(participants).build(); + } +} \ No newline at end of file From e6eee42de7c54ccd045313dd98a81b65e48e793f Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 17:33:19 +0900 Subject: [PATCH 10/15] =?UTF-8?q?test=20:=20ChatRoomService.class=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatRoomServiceTest.java | 162 ++++++++++++++++-- 1 file changed, 147 insertions(+), 15 deletions(-) diff --git a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java index 5d7915dd..4d89c804 100644 --- a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java +++ b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java @@ -1,11 +1,15 @@ package inu.codin.codin.domain.chat.service; -import inu.codin.codin.common.security.util.SecurityUtils; +import inu.codin.codin.common.exception.NotFoundException; import inu.codin.codin.domain.block.service.BlockService; import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; +import inu.codin.codin.domain.chat.domain.chatroom.Participants; import inu.codin.codin.domain.chat.domain.chatroom.event.ChatRoomNotificationEvent; import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; import inu.codin.codin.domain.chat.dto.chatroom.response.ChatRoomCreateResponseDto; +import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; +import inu.codin.codin.domain.chat.exception.ChatRoomException; +import inu.codin.codin.domain.chat.exception.ChatRoomExistedException; import inu.codin.codin.domain.chat.repository.ChatRoomRepository; import inu.codin.codin.domain.user.security.CustomUserDetails; import org.bson.types.ObjectId; @@ -19,12 +23,17 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ChatRoomServiceTest { @@ -34,17 +43,19 @@ class ChatRoomServiceTest { @Mock ChatRoomRepository chatRoomRepository; @Mock - SecurityUtils securityUtils; - @Mock ChatRoomValidator chatRoomValidator; @Mock BlockService blockService; @Mock ApplicationEventPublisher eventPublisher; + ObjectId chatRoomId = new ObjectId("64a9f9b8e1c4c3a1d4e5f677"); + ObjectId senderId = new ObjectId("64a9f9b8e1c4c3a1d4e5f678"); + ObjectId receiverId = new ObjectId("64a9f9b8e1c4c3a1d4e5f679"); + @BeforeEach public void setCustomDetails(){ - CustomUserDetails userDetails = CustomUserDetails.builder().email("test@test.com").build(); + CustomUserDetails userDetails = CustomUserDetails.builder().id(senderId).email("test@test.com").build(); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); @@ -54,16 +65,16 @@ public void setCustomDetails(){ @DisplayName("채팅방을 생성합니다.") public void 채팅방_생성(){ //given - ObjectId senderId = new ObjectId(); - given(SecurityUtils.getCurrentUserId()).willReturn(senderId); - ChatRoomCreateRequestDto requestDto = ChatRoomCreateRequestDto.builder() - .roomName("roomName") - .receiverId("receiverId") - .referenceId("referenceId") + .receiverId(receiverId.toString()) + .referenceId(new ObjectId().toString()) .build(); - ChatRoom chatRoom = ChatRoom.of(requestDto, senderId); - given(chatRoomRepository.save(any(ChatRoom.class))).willReturn(chatRoom); + + given(chatRoomRepository.save(any(ChatRoom.class))).willAnswer(invocation -> { //생성된 ChatRoom의 _id를 동적으로 채워줌 + ChatRoom savedChatRoom = invocation.getArgument(0); + ReflectionTestUtils.setField(savedChatRoom, "_id", chatRoomId); + return savedChatRoom; + }); //when ChatRoomCreateResponseDto createResponseDto = chatRoomService.createChatRoom(requestDto); @@ -71,13 +82,134 @@ public void setCustomDetails(){ //then verify(chatRoomRepository, times(1)).save(any(ChatRoom.class)); verify(eventPublisher, times(1)).publishEvent(any(ChatRoomNotificationEvent.class)); - assertThat(createResponseDto.getChatRoomId()).isEqualTo(chatRoom.get_id().toString()); + + assertThat(createResponseDto.getChatRoomId()).isEqualTo(chatRoomId.toString()); } @Test @DisplayName("채팅방 생성 중 오류 발생 - Receiver가 존재하는지 확인 NotFoundException") public void 채팅방_오류_NotFoundException(){ + //given + ChatRoomCreateRequestDto requestDto = mock(ChatRoomCreateRequestDto.class); + doThrow(new NotFoundException("Receive 유저를 찾을 수 없습니다.")) + .when(chatRoomValidator).validate(requestDto, senderId); + + //when & then + assertThatThrownBy(() -> chatRoomService.createChatRoom(requestDto)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Receive 유저를 찾을 수 없습니다."); + + verify(chatRoomRepository, never()).save(any()); + verify(eventPublisher, never()).publishEvent(any()); + } + + @Test + @DisplayName("채팅방 생성 중 오류 발생 - 중복된 채팅방이 존재하는 지 확인 ChatRoomExistedException") + public void 채팅방_오류_ChatRoomExistedException(){ + //given + ChatRoomCreateRequestDto requestDto = mock(ChatRoomCreateRequestDto.class); + doThrow(new ChatRoomExistedException(chatRoomId)) + .when(chatRoomValidator).validate(requestDto, senderId); + + //when & then + assertThatThrownBy(() -> chatRoomService.createChatRoom(requestDto)) + .isInstanceOf(ChatRoomExistedException.class) + .hasMessage(ChatRoomErrorCode.CHATROOM_EXISTED.message()); + + verify(chatRoomRepository, never()).save(any()); + verify(eventPublisher, never()).publishEvent(any()); + } + + @Test + @DisplayName("채팅방 생성 중 오류 발생 - 자기 자신과 채팅 불가 ChatRoomException") + void 채팅방_오류_ChatRoomException(){ + //given + ChatRoomCreateRequestDto requestDto = mock(ChatRoomCreateRequestDto.class); + doThrow(new ChatRoomException(ChatRoomErrorCode.CHATROOM_CREATE_MYSELF)) + .when(chatRoomValidator).validate(requestDto, senderId); + + //when & then + assertThatThrownBy(() -> chatRoomService.createChatRoom(requestDto)) + .isInstanceOf(ChatRoomException.class) + .hasMessage(ChatRoomErrorCode.CHATROOM_CREATE_MYSELF.message()); + + verify(chatRoomRepository, never()).save(any()); + verify(eventPublisher, never()).publishEvent(any()); + } + + @Test + @DisplayName("채팅방 목록 전체 조회") + void 채팅방_목록_전체_조회(){ + //blockedUser 해결 후 테스트 진행 + } + + @Test + @DisplayName("채팅방 조회 실패 시 에러 반환") + void 채팅방_조회_실패_ChatRoomException(){ + //given + given(chatRoomRepository.findBy_idAndDeletedAtIsNull(chatRoomId)).willReturn(Optional.empty()); + + //when & then + assertThatThrownBy(() -> chatRoomService.getChatRoom(chatRoomId.toString())) + .isInstanceOf(ChatRoomException.class) + .hasMessage(ChatRoomErrorCode.CHATROOM_NOT_FOUND.message()); + } + + @Test + @DisplayName("채팅방 나가기") + void 채팅방_나가기(){ + //given + ChatRoom chatRoom = getChatRoom(); + given(chatRoomRepository.findBy_idAndDeletedAtIsNull(chatRoomId)).willReturn(Optional.ofNullable(chatRoom)); + + //when + chatRoomService.leaveChatRoom(chatRoomId.toString()); + + //given + assertTrue(chatRoom.getParticipants().getInfo().get(senderId).isLeaved()); + verify(chatRoomRepository, times(1)).save(any()); + verify(chatRoomValidator, times(1)).validateParticipantsAllLeaved(any(), any()); + } + + @Test + @DisplayName("채팅방 알림 설정 true -> false") + void 채팅방_알림_끄기(){ + //given + ChatRoom chatRoom = getChatRoom(); //채팅방 생성 시 알림 true + given(chatRoomRepository.findBy_idAndDeletedAtIsNull(chatRoomId)).willReturn(Optional.of(chatRoom)); + + //when + //알림 false로 설정 + chatRoomService.setNotificationChatRoom(chatRoomId.toString()); + + //then + assertFalse(chatRoom.getParticipants().getInfo().get(senderId).isNotificationsEnabled()); + verify(chatRoomRepository, times(1)).save(any()); + } + + @Test + @DisplayName("채팅방 알림 설정 false -> true") + void 채팅방_알림_켜기(){ + //given + ChatRoom chatRoom = getChatRoom(); + chatRoom.getParticipants().getInfo().get(senderId).updateNotification(); + //채팅방 알림 false + given(chatRoomRepository.findBy_idAndDeletedAtIsNull(chatRoomId)).willReturn(Optional.of(chatRoom)); + + //when + //알림 true로 설정 + chatRoomService.setNotificationChatRoom(chatRoomId.toString()); + + //then + assertTrue(chatRoom.getParticipants().getInfo().get(senderId).isNotificationsEnabled()); + verify(chatRoomRepository, times(1)).save(any()); + } + + private ChatRoom getChatRoom() { + Participants participant = new Participants(); + participant.create(senderId); + return ChatRoom.builder().participants(participant).build(); } } \ No newline at end of file From 20ac6395d242a9edf22f6e2b072e2c8a4cb01a50 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Wed, 2 Jul 2025 17:38:25 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor=20:=20ParticipantsInfo=EB=A5=BC?= =?UTF-8?q?=20=EC=BA=A1=EC=8A=90=ED=99=94=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20Participants=EC=97=90=EC=84=9C=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=ED=9B=84=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/chatroom/Participants.java | 18 +++++++++++++++--- .../domain/chat/service/ChatRoomValidator.java | 11 +++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java index 5d0aa627..b12410ad 100644 --- a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java @@ -41,6 +41,11 @@ public List getDisconnectedUsersAndUpdateUnreadCount(ObjectId send }).filter(Objects::nonNull).toList(); } + private ParticipantInfo findParticipant(ObjectId userId) { + return Optional.ofNullable(info.get(userId)) + .orElseThrow(() -> new ChatRoomException(ChatRoomErrorCode.PARTICIPANTS_NOT_FOUND)); + } + public void enter(ObjectId userId) { findParticipant(userId).connect(); } @@ -88,9 +93,16 @@ public int getUnreadCount(ObjectId userId) { return findParticipant(userId).getUnreadCount(); } - private ParticipantInfo findParticipant(ObjectId userId) { - return Optional.ofNullable(info.get(userId)) - .orElseThrow(() -> new ChatRoomException(ChatRoomErrorCode.PARTICIPANTS_NOT_FOUND)); + public boolean checkAllLeaved() { + return info.values().stream().allMatch(ParticipantInfo::isLeaved); + } + + public boolean isLeaved(ObjectId senderId) { + return findParticipant(senderId).isLeaved(); + } + + public void remain(ObjectId senderId) { + findParticipant(senderId).remain(); } public record ReceiverInfo(ObjectId receiverId, int unreadMessage){} diff --git a/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java index 31b4ae32..d5357e4b 100644 --- a/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java +++ b/src/main/java/inu/codin/codin/domain/chat/service/ChatRoomValidator.java @@ -3,6 +3,7 @@ import inu.codin.codin.common.exception.NotFoundException; import inu.codin.codin.domain.chat.domain.chatroom.ChatRoom; import inu.codin.codin.domain.chat.domain.chatroom.ParticipantInfo; +import inu.codin.codin.domain.chat.domain.chatroom.Participants; import inu.codin.codin.domain.chat.dto.chatroom.request.ChatRoomCreateRequestDto; import inu.codin.codin.domain.chat.exception.ChatRoomErrorCode; import inu.codin.codin.domain.chat.exception.ChatRoomException; @@ -39,9 +40,9 @@ private void validateChatRoomDuplicated(ChatRoomCreateRequestDto chatRoomCreateR Optional existedChatroom = chatRoomRepository.findByReferenceIdAndParticipantsContaining( chatRoomCreateRequestDto.getReferenceId(), senderId, chatRoomCreateRequestDto.getReceiverId()); if (existedChatroom.isPresent()) { - ParticipantInfo participantInfo= existedChatroom.get().getParticipants().getInfo().get(senderId); - if (participantInfo.isLeaved()) { - participantInfo.remain(); + Participants participants = existedChatroom.get().getParticipants(); + if (participants.isLeaved(senderId)){ + participants.remain(senderId); chatRoomRepository.save(existedChatroom.get()); } //client 측에서 303 에러를 받아서 해당 채팅방으로 redirect @@ -58,9 +59,7 @@ private void validateReceiverExisted(ChatRoomCreateRequestDto chatRoomCreateRequ } public void validateParticipantsAllLeaved(String chatRoomId, ChatRoom chatRoom) { - boolean isAllLeaved = chatRoom.getParticipants().getInfo().values() - .stream() - .allMatch(ParticipantInfo::isLeaved); // 모든 참가자가 떠났는지 확인 + boolean isAllLeaved = chatRoom.getParticipants().checkAllLeaved(); // 모든 참가자가 떠났는지 확인 if (isAllLeaved) { chatRoom.delete(); chatRoomRepository.save(chatRoom); From ad23e9186a95cd5f9a5a9d61d09a10dbafac050f Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Fri, 4 Jul 2025 16:05:16 +0900 Subject: [PATCH 12/15] =?UTF-8?q?refactor=20:=20ParticipantInfo=EC=9D=98?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20Participants=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=9D=80?= =?UTF-8?q?=EB=8B=89=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EC=A0=9C=EC=96=B4=EC=9E=90=EB=A5=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20default=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/chatroom/ParticipantInfo.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java index 54c3a979..e252f11e 100644 --- a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/ParticipantInfo.java @@ -21,41 +21,41 @@ public ParticipantInfo(ObjectId userId) { this.userId = userId; } - public static ParticipantInfo enter(ObjectId userId) { + static ParticipantInfo enter(ObjectId userId) { return new ParticipantInfo(userId); } - public void updateNotification() { + void updateNotification() { this.notificationsEnabled = !notificationsEnabled; } - public void incrementUnreadCount() { + void incrementUnreadCount() { this.unreadCount++; } - public void connect() { + void connect() { this.isConnected = true; this.unreadCount = 0; } - public void disconnect() { + void disconnect() { this.isConnected = false; this.unreadCount = 0; setUpdatedAt(); } - public void leave() { + void leave() { this.isLeaved = true; this.whenLeaved = LocalDateTime.now(); disconnect(); } - public void remain() { + void remain() { this.isLeaved = false; } - public boolean isNotified(ObjectId userId){ - return !this.userId.equals(userId) && this.notificationsEnabled & !this.isConnected; + boolean isNotified(ObjectId userId) { + return !this.userId.equals(userId) && this.notificationsEnabled && !this.isConnected; } } From bf878e0f486508dc68be751261f1263b66439bd2 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Fri, 4 Jul 2025 16:05:31 +0900 Subject: [PATCH 13/15] =?UTF-8?q?doc=20:=20Event=EA=B4=80=EB=A0=A8=20Log?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codin/domain/chat/event/ChattingEventListener.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java b/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java index 0be6b478..5c1c5a06 100644 --- a/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java +++ b/src/main/java/inu/codin/codin/domain/chat/event/ChattingEventListener.java @@ -10,6 +10,7 @@ import inu.codin.codin.domain.notification.service.NotificationService; import inu.codin.codin.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.SimpMessageSendingOperations; @@ -25,6 +26,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class ChattingEventListener { private final ChatRoomRepository chatRoomRepository; @@ -44,6 +46,7 @@ public void handleChattingArrivedEvent(ChattingArrivedEvent event){ updateLastMessage(event); updateUnreadCountAndNotify(event.getChatting(), event.getChatRoom()); chatRoomRepository.save(event.getChatRoom()); + log.info("[handleChattingArrivedEvent] ChattingArrivedEvent 완료"); } private void updateLastMessage(ChattingArrivedEvent event){ @@ -67,6 +70,7 @@ private Map getLastMessageAndUnread(Chatting chatting, int unrea private void sendUnreadCountChange(ObjectId receiverId, Map result) { userRepository.findByUserId(receiverId) .ifPresent(userEntity -> template.convertAndSendToUser(userEntity.getEmail(), "/queue/chatroom/unread", result)); + log.info("[sendUnreadCountChange] user : {}, /queue/chatroom/unread 전송 완료", receiverId); } /** @@ -79,6 +83,7 @@ public void handleChattingNotificationEvent(ChattingNotificationEvent event){ ChatRoom chatRoom = event.getChatRoom(); chatRoom.getParticipants().getUsersToNotify(event.getUserId()) .forEach(userId -> notificationService.sendNotificationMessageByChat(userId, chatRoom.get_id())); + log.info("[handleChattingNotificationEvent] ChattingNotificationEvent 완료"); } /** @@ -95,6 +100,7 @@ public void updateUnreadCountEvent(ChattingUnreadCountEvent chattingUnreadCountE )).toList(); template.convertAndSend("/queue/unread/"+ chattingUnreadCountEvent.getChatRoomId(), unreadChattingList); + log.info("[updateUnreadCountEvent] ChattingUnreadCountEvent 완료"); } } From 665dbce6f92d85b02ce6f932de9283fff83a235b Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Fri, 4 Jul 2025 16:05:56 +0900 Subject: [PATCH 14/15] =?UTF-8?q?refactor=20:=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20stream=EC=9C=BC=EB=A1=9C=20=EB=A1=9C=EC=A7=81=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/chatroom/Participants.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java index b12410ad..a9c05a37 100644 --- a/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java +++ b/src/main/java/inu/codin/codin/domain/chat/domain/chatroom/Participants.java @@ -29,16 +29,15 @@ public void create(ObjectId memberId){ * @return 메세지를 받는 유저들의 정보 리스트 */ public List getDisconnectedUsersAndUpdateUnreadCount(ObjectId senderId) { - return info.keySet().stream().map(receiverId -> { - if (!receiverId.equals(senderId)) { - ParticipantInfo receiverInfo = info.get(receiverId); - if (!receiverInfo.isConnected()){ + return info.keySet().stream() + .filter(receiverId -> !receiverId.equals(senderId)) + .map(info::get) + .filter(receiverInfo -> !receiverInfo.isConnected()) + .map(receiverInfo -> { receiverInfo.incrementUnreadCount(); - return new ReceiverInfo(receiverId, receiverInfo.getUnreadCount()); - } - } - return null; - }).filter(Objects::nonNull).toList(); + return new ReceiverInfo(receiverInfo.getUserId(), receiverInfo.getUnreadCount()); + }) + .toList(); } private ParticipantInfo findParticipant(ObjectId userId) { @@ -73,9 +72,14 @@ public LocalDateTime getWhenLeaved(ObjectId userId) { } public List remainReceiver(ObjectId userId) { + List participantInfos = findReceiversToRemain(userId); + participantInfos.forEach(ParticipantInfo::remain); + return participantInfos; + } + + private List findReceiversToRemain(ObjectId userId){ return info.values().stream() .filter(info -> !info.getUserId().equals(userId) && info.isLeaved()) - .peek(ParticipantInfo::remain) .toList(); } From 1794349a84372364618e9fd3b618433c12659747 Mon Sep 17 00:00:00 2001 From: X1n9fU Date: Fri, 4 Jul 2025 16:06:34 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor=20:=20ParticipantsInfo=20?= =?UTF-8?q?=EC=9D=80=EB=8B=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codin/codin/domain/chat/service/ChatRoomServiceTest.java | 2 +- .../codin/codin/domain/chat/service/ChatRoomValidatorTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java index 4d89c804..0628b639 100644 --- a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java +++ b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomServiceTest.java @@ -192,7 +192,7 @@ public void setCustomDetails(){ void 채팅방_알림_켜기(){ //given ChatRoom chatRoom = getChatRoom(); - chatRoom.getParticipants().getInfo().get(senderId).updateNotification(); + chatRoom.getParticipants().toggleNotification(senderId); //채팅방 알림 false given(chatRoomRepository.findBy_idAndDeletedAtIsNull(chatRoomId)).willReturn(Optional.of(chatRoom)); diff --git a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java index f7d36c4a..56a595ab 100644 --- a/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java +++ b/src/test/java/inu/codin/codin/domain/chat/service/ChatRoomValidatorTest.java @@ -131,7 +131,7 @@ private ChatRoom getChatRoom() { private ChatRoom getChatRoomWithLeaveParticipants() { Participants participants = new Participants(); participants.create(senderId); - participants.getInfo().get(senderId).leave(); + participants.leave(senderId); return ChatRoom.builder().participants(participants).build(); } } \ No newline at end of file