Skip to content
Merged
21 changes: 21 additions & 0 deletions src/main/java/com/manchui/domain/controller/ChatController.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.security.Principal;
import java.time.LocalDateTime;

@RestController
Expand Down Expand Up @@ -54,6 +56,25 @@ public Mono<ResponseEntity<SuccessResponse<Void>>> chat(@DestinationVariable Str
}).then(Mono.just(ResponseEntity.ok().body(SuccessResponse.successWithNoData("메시지 전송 성공"))));
}

@MessageMapping("chat.leave.{roomId}")
public Mono<ResponseEntity<SuccessResponse<Void>>> chatRoomLeave(@DestinationVariable String roomId,
@RequestBody ChatMessageRequest chatMessageRequest,
Principal principal){

return chatMessageService.chatQuiteMessageSave(chatMessageRequest, roomId).flatMap(chatMessage -> {
// 채팅방 나가기 메시지 전송
rabbitTemplate.convertAndSend("chat.exchange", "room." + roomId, new ChatMessageResponse(
chatMessageRequest.getSender(), chatMessageRequest.getSender() + chatMessageRequest.getMessage(), chatMessage.getChatMessageType(), LocalDateTime.now()));

// 블로킹 JPA 메서드는 별도 스레드에서 실행
// 채팅방 유저 목록에서 유저 softDelete
return Mono.fromCallable(() -> {
chatRoomService.chatRoomQuite(principal.getName(), roomId);
return null;
}).subscribeOn(Schedulers.boundedElastic());
}).then(Mono.just(ResponseEntity.ok().body(SuccessResponse.successWithNoData("채팅방 나기기 성공"))));
}

@GetMapping("/api/chat/user/list/{roomId}")
public ResponseEntity<SuccessResponse<ChatRoomUserListResponse>> chatRoomUserList(@PathVariable String roomId) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.manchui.domain.dto.chat;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ChatMessageRequest {

private String sender;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ public class ChatRoomListDetail {
private int userNum;
private LocalDateTime lastMessageTime;
private String lastMessage;
private String lastMessageUserName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public enum ChatMessageType {

ENTER, MESSAGE
ENTER, MESSAGE, QUITE, OPEN
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

public interface ChatRoomUserRepository extends JpaRepository<ChatRoomUser, Long> {

List<ChatRoomUser> findByChatRoomEquals(ChatRoom chatRoom);
List<ChatRoomUser> findByChatRoomEqualsAndDeletedAtIsNull(ChatRoom chatRoom);

Optional<ChatRoomUser> findByUserEqualsAndChatRoomEqualsAndDeletedAtIsNull(User user, ChatRoom chatRoom);

Optional<ChatRoomUser> findByUserEqualsAndChatRoomEquals(User user, ChatRoom chatRoom);

List<ChatRoomUser> findByUser(User user);
List<ChatRoomUser> findByUserAndDeletedAtIsNull(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;

public interface ChatMessageRepository extends ReactiveMongoRepository<ChatMessage, String> {

Flux<ChatMessage> findByRoomIdOrderByCreatedAtDesc(String roomId);
Flux<ChatMessage> findByRoomIdAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc(String roomId, LocalDateTime createdAt);

@Query(value = "{ 'roomId': ?0, '_id': { $lt: ?1 } }", sort = "{ '_id': -1 }")
Flux<ChatMessage> findByRoomIdAndIdLessThanOrderByIdDesc(String roomId, ObjectId lastMessageId);
@Query(value = "{ 'roomId': ?0, '_id': { $lt: ?1 }, 'createdAt': { $lt: ?2 } }", sort = "{ '_id': -1 }")
Flux<ChatMessage> findByRoomIdAndIdLessThanAndCreatedAtGreaterThanEqualOrderByIdDesc(String roomId, ObjectId lastMessageId, LocalDateTime createdAt);

Mono<ChatMessage> findFirstByRoomIdOrderByCreatedAtDesc(String roomId);
}
92 changes: 65 additions & 27 deletions src/main/java/com/manchui/domain/service/ChatMessageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
import org.bson.types.ObjectId;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuples;

import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -35,6 +38,7 @@ public class ChatMessageService {
private final UserRepository userRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatRoomUserRepository chatRoomUserRepository;
private final TransactionTemplate transactionTemplate;

// 채팅 저장 메서드
public Mono<ChatMessage> chatMessageSave(ChatMessageRequest chatMessageRequest, String roomId) {
Expand All @@ -43,46 +47,80 @@ public Mono<ChatMessage> chatMessageSave(ChatMessageRequest chatMessageRequest,
chatMessageRequest.getMessage(), LocalDateTime.now()));
}

public Mono<ChatMessage> chatQuiteMessageSave(ChatMessageRequest chatMessageRequest, String roomId){

return chatMessageRepository.save(new ChatMessage(roomId, ChatMessageType.QUITE,chatMessageRequest.getSender(),
chatMessageRequest.getSender() + chatMessageRequest.getMessage(), LocalDateTime.now()));
}

public Mono<ChatMessage> chatRoomOpenMessageSave(ChatMessageRequest chatMessageRequest, String roomId){
return chatMessageRepository.save(new ChatMessage(roomId, ChatMessageType.OPEN,chatMessageRequest.getSender(),
chatMessageRequest.getMessage(), LocalDateTime.now()));
}

//채팅방 입장 및 채팅 목록 조회 메서드
@Transactional
public Mono<ChatMessageSliceResponse> findChatList(CustomUserDetails customUserDetails, String roomId, ObjectId lastMessageId, int limit,
RabbitTemplate rabbitTemplate) {
// Blocking(JPA) 작업을 다른 스레드풀에서 수행하기 위해 fromCallable 사용
return Mono.fromCallable(() -> {

// 블로킹(JPA) 코드 실행 영역.
// 이벤트 루프가 아닌 별도 쓰레드에서 처리할 예정이므로,
// 여기서 JPA 호출을 수행해도 WebFlux 이벤트 루프를 방해하지 않는다.

// 유저 정보와 채팅방 정보를 블로킹 방식으로 조회
String userEmail = customUserDetails.getUsername();
User user = userRepository.findByEmail(userEmail);
ChatRoom chatRoom = chatRoomRepository.findByRoomId(roomId);

// 해당 유저가 채팅방 참여자가 맞는지 확인 후, 없으면 새로 저장 및 입장 메시지
Optional<ChatRoomUser> chatRoomUser = chatRoomUserRepository.findByUserEqualsAndChatRoomEquals(user, chatRoom);
if (chatRoomUser.isEmpty()) {
chatRoomUserRepository.save(new ChatRoomUser(user, chatRoom));
chatMessageRepository.save(new ChatMessage(roomId, ChatMessageType.ENTER, user.getName(), user.getName() + " 님이 입장 하셨습니다.", LocalDateTime.now())).block();
rabbitTemplate.convertAndSend("chat.exchange", "room." + roomId, new ChatMessageResponse(
user.getName(), user.getName() + " 님이 입장 하셨습니다.", ChatMessageType.MESSAGE,LocalDateTime.now()));
}
return Mono.fromCallable(() ->
transactionTemplate.execute(status -> {
// 블로킹(JPA) 코드 실행 영역.
// 이벤트 루프가 아닌 별도 쓰레드에서 처리할 예정이므로,
// 여기서 JPA 호출을 수행해도 WebFlux 이벤트 루프를 방해하지 않는다.

// 블로킹 영역에서 최종적으로 roomId만 반환
// (이후 flatMapMany로 ReactiveMongoRepository를 호출하기 위해서)
return roomId;
})
// 유저 정보와 채팅방 정보를 블로킹 방식으로 조회
String userEmail = customUserDetails.getUsername();
User user = userRepository.findByEmail(userEmail);
ChatRoom chatRoom = chatRoomRepository.findByRoomId(roomId);

// 해당 유저가 채팅방 참여자가 맞는지 확인 후, 없으면 새로 저장 및 입장 메시지
// DeleteatAt 상관없이 조회 후에 delete 가 null 이면 그냥 새로운 채팅방 회원, deleteatat의 값이 있으면 deleatedat null로 변환후,
Optional<ChatRoomUser> chatRoomUser = chatRoomUserRepository.findByUserEqualsAndChatRoomEquals(user, chatRoom);
LocalDateTime chatRoomUserUpdatedAt = null;
// 채팅방에 유저가 속해있으면 채팅방 입장 시간 조회
if (chatRoomUser.isPresent()) {
chatRoomUserUpdatedAt = chatRoomUser.get().getUpdatedAt();
}

if (chatRoomUser.isEmpty()) {
// 채팅방 첫입장
chatRoomUserUpdatedAt = chatRoomUserRepository.save(new ChatRoomUser(user, chatRoom)).getUser().getUpdatedAt();
chatMessageRepository.save(new ChatMessage(roomId, ChatMessageType.ENTER, user.getName(), user.getName() + " 님이 입장 하셨습니다.", LocalDateTime.now())).block();
rabbitTemplate.convertAndSend("chat.exchange", "room." + roomId, new ChatMessageResponse(
user.getName(), user.getName() + " 님이 입장 하셨습니다.", ChatMessageType.ENTER, LocalDateTime.now()));
}else if(chatRoomUser.get().getDeletedAt() != null){
// 채팅방 재입장
chatRoomUser.get().restore();
chatRoomUser.get().updateTime();
chatRoomUserRepository.flush();
chatRoomUserUpdatedAt = chatRoomUser.get().getUpdatedAt();

chatMessageRepository.save(new ChatMessage(roomId, ChatMessageType.ENTER, user.getName(), user.getName() + " 님이 입장 하셨습니다.", chatRoomUserUpdatedAt)).block();
rabbitTemplate.convertAndSend("chat.exchange", "room." + roomId, new ChatMessageResponse(
user.getName(), user.getName() + " 님이 입장 하셨습니다.", ChatMessageType.ENTER, chatRoomUserUpdatedAt));
}

// 블로킹 영역에서 최종적으로 roomId만 반환
// (이후 flatMapMany로 ReactiveMongoRepository를 호출하기 위해서)
return Tuples.of(roomId, chatRoomUserUpdatedAt);
})
)
// subscribeOn: 위의 fromCallable 블록을 별도의 쓰레드 풀(boundedElastic)에서 실행
.subscribeOn(Schedulers.boundedElastic())
// flatMapMany로 넘겨 받은 roomId로 리액티브 MongoDB 쿼리 수행
.flatMapMany(rId -> {
.flatMapMany(tuple -> {

String rId = tuple.getT1();
LocalDateTime updatedAt = tuple.getT2();
// lastMessageId == null이면 최신 메시지 조회
if (lastMessageId == null) {
// 초기 요청 (최신 메시지 limit + 1개)
return chatMessageRepository.findByRoomIdOrderByCreatedAtDesc(rId)
.take(limit + 1); // // limit+1개 가져와서 다음 페이지 여부 확인
return chatMessageRepository.findByRoomIdAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc(rId, updatedAt)
.take(limit + 1); // limit+1개 가져와서 다음 페이지 여부 확인
} else {
// 이후 요청 (lastMessageId 기준)
return chatMessageRepository.findByRoomIdAndIdLessThanOrderByIdDesc(rId, lastMessageId)
return chatMessageRepository.findByRoomIdAndIdLessThanAndCreatedAtGreaterThanEqualOrderByIdDesc(rId, lastMessageId, updatedAt)
.take(limit + 1);
}
})
Expand Down
23 changes: 19 additions & 4 deletions src/main/java/com/manchui/domain/service/ChatRoomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import com.manchui.global.exception.CustomException;
import com.manchui.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import java.util.Comparator;
Expand All @@ -21,6 +23,7 @@

@Service
@RequiredArgsConstructor
@Slf4j
public class ChatRoomService {

private final ChatRoomUserRepository chatRoomUserRepository;
Expand All @@ -35,7 +38,7 @@ public ChatRoomUserListResponse chatRoomUserList(String roomId) {

ChatRoom chatRoom = chatRoomRepository.findByRoomId(roomId);

List<ChatRoomUser> userList = chatRoomUserRepository.findByChatRoomEquals(chatRoom);
List<ChatRoomUser> userList = chatRoomUserRepository.findByChatRoomEqualsAndDeletedAtIsNull(chatRoom);

List<UserInfo> userInfoList = userList.stream().map(m -> new UserInfo(
m.getUser().getName(),
Expand All @@ -51,21 +54,33 @@ public ChatRoomListResponse chatRoomList(CustomUserDetails customUserDetails) {
String userEmail = customUserDetails.getUsername();
User user = userRepository.findByEmail(userEmail);
// 사용자가 속하 ChatRoomUser 조회
List<ChatRoomUser> chatRoomUsers = chatRoomUserRepository.findByUser(user);
List<ChatRoomUser> chatRoomUsers = chatRoomUserRepository.findByUserAndDeletedAtIsNull(user);
// ChatRoomUser -> DTO(ChatRoomListDetail) 변환
List<ChatRoomListDetail> chatRoomListDetails = chatRoomUsers.stream().map((m -> {
ChatRoom chatRoom = m.getChatRoom();
Gathering gathering = gatheringRepository.findByChatRoomEquals(chatRoom).orElseThrow(
() -> new CustomException(ErrorCode.GATHERING_NOT_FOUND));

Image image = imageRepository.findByGatheringId(gathering.getId());
List<ChatRoomUser> chatRoomEquals = chatRoomUserRepository.findByChatRoomEquals(chatRoom);
List<ChatRoomUser> chatRoomEquals = chatRoomUserRepository.findByChatRoomEqualsAndDeletedAtIsNull(chatRoom);

ChatMessage lastMessage = chatMessageRepository.findFirstByRoomIdOrderByCreatedAtDesc(chatRoom.getRoomId()).block();
return new ChatRoomListDetail(m.getChatRoom().getRoomId(), image.getFilePath(), gathering.getGroupName(),
chatRoomEquals.size(), lastMessage.getCreatedAt(), lastMessage.getMessage());
chatRoomEquals.size(), lastMessage.getCreatedAt(), lastMessage.getMessage(), lastMessage.getSender());
})).sorted(Comparator.comparing(ChatRoomListDetail::getLastMessageTime).reversed()).collect(Collectors.toList());

return new ChatRoomListResponse(chatRoomListDetails);
}

// 채팅방에 속한 사용자 softDelete
@Transactional
public void chatRoomQuite(String email, String roomId){

User user = userRepository.findByEmail(email);
ChatRoom chatRoom = chatRoomRepository.findByRoomId(roomId);
ChatRoomUser chatRoomUser = chatRoomUserRepository.findByUserEqualsAndChatRoomEqualsAndDeletedAtIsNull(user, chatRoom).orElseThrow(
() -> new CustomException(ErrorCode.MEMBER_NOT_IN_CHATROOM)
);
chatRoomUser.softDelete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.manchui.domain.dto.CustomUserDetails;
import com.manchui.domain.dto.UserInfo;
import com.manchui.domain.dto.chat.ChatMessageRequest;
import com.manchui.domain.dto.gathering.*;
import com.manchui.domain.dto.review.ReviewDetailPagingResponse;
import com.manchui.domain.dto.review.ReviewInfo;
Expand Down Expand Up @@ -57,6 +58,9 @@ public class GatheringServiceImpl implements GatheringService {

private final ChatRoomUserRepository chatRoomUserRepository;

private final ChatMessageService chatMessageService;


/**
* 0. 모임 생성
* 작성자 : 오예령
Expand Down Expand Up @@ -117,8 +121,10 @@ public GatheringCreateResponse createGathering(String email, GatheringCreateRequ
} else {
// 2. 모임 및 이미지 객체 저장
ChatRoom chatRoom = new ChatRoom(UUID.randomUUID().toString());
chatRoomRepository.save(chatRoom);
String roomId = chatRoomRepository.save(chatRoom).getRoomId();
chatRoomUserRepository.save(new ChatRoomUser(user, chatRoom));
chatMessageService.chatRoomOpenMessageSave(new ChatMessageRequest(user.getName(), user.getName() + "님이 채팅방을 개설하였습니다."), roomId).block();

Gathering gathering = gatheringStore.saveGathering(createRequest, user, gatheringDate, dueDate, chatRoom);
imageService.uploadGatheringImage(createRequest.getGatheringImage(), gathering.getId(), false);

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/manchui/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public enum ErrorCode {
NOTIFICATION_CANNOT_BE_DELETED(HttpStatus.BAD_REQUEST, "삭제할 수 없는 알림입니다.?"),

// chat
CHATROOM_NOT_FOUND(HttpStatus.BAD_REQUEST, "존재하지 않는 채팅방 입니다.");
CHATROOM_NOT_FOUND(HttpStatus.BAD_REQUEST, "존재하지 않는 채팅방 입니다."),
MEMBER_NOT_IN_CHATROOM(HttpStatus.NOT_FOUND, "회원이 해당 채팅방에 속하지 않습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Loading