diff --git a/src/main/java/sumcoda/boardbuddy/config/WebsocketConfig.java b/src/main/java/sumcoda/boardbuddy/config/WebsocketConfig.java new file mode 100644 index 00000000..e4908922 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/config/WebsocketConfig.java @@ -0,0 +1,36 @@ +package sumcoda.boardbuddy.config; + +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +@EnableWebSocketMessageBroker +public class WebsocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * 메시지 브로커 설정 + * + * @param registry 메시지 브로커 레지스트리 + **/ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/api/chat"); + + registry.enableSimpleBroker("/api/chat/reception"); + } + + /** + * STOMP 엔드포인트 등록 + * + * @param registry STOMP 엔드포인트 레지스트리 + **/ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/api/chat/connection") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/controller/ChatMessageController.java b/src/main/java/sumcoda/boardbuddy/controller/ChatMessageController.java new file mode 100644 index 00000000..b5bd0ed8 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/controller/ChatMessageController.java @@ -0,0 +1,61 @@ +package sumcoda.boardbuddy.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; +import sumcoda.boardbuddy.dto.ChatMessageRequest; +import sumcoda.boardbuddy.dto.ChatMessageResponse; +import sumcoda.boardbuddy.dto.common.ApiResponse; +import sumcoda.boardbuddy.service.ChatMessageService; + +import java.util.List; +import java.util.Map; + +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildSuccessResponseWithPairKeyData; + +@Controller +@RequiredArgsConstructor +public class ChatMessageController { + + private final ChatMessageService chatMessageService; + + /** + * 특정 채팅방에 메세지 발행 및 전송 + * + * @param chatRoomId 채팅방 Id + * @param publishDTO 발행할 메세지 내용 DTO + * @param username 메시지를 발행하는 사용자 이름 + **/ + @MessageMapping("/publication/{chatRoomId}") + public void publishMessage( + @DestinationVariable Long chatRoomId, + @Payload ChatMessageRequest.PublishDTO publishDTO, + @RequestAttribute String username) { + + chatMessageService.publishMessage(chatRoomId, publishDTO, username); + } + + /** + * 채팅방 메세지 내역 조회 + * + * @param chatRoomId 채팅방 Id + * @param username 요청을 보낸 사용자 아이디 + * @return 채팅방 메세지 내역 + */ + @GetMapping("/api/chat/rooms/{chatRoomId}/messages") + public ResponseEntity>>> getChatMessages( + @PathVariable Long chatRoomId, + @RequestAttribute String username) { + + List chatMessages = chatMessageService.findMessagesAfterMemberJoinedByChatRoomIdAndUsername(chatRoomId, username); + + return buildSuccessResponseWithPairKeyData("chatMessages", chatMessages, "채팅 메세지들의 정보를 성공적으로 조회했습니다.", HttpStatus.OK); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/controller/ChatRoomController.java b/src/main/java/sumcoda/boardbuddy/controller/ChatRoomController.java new file mode 100644 index 00000000..c7601408 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/controller/ChatRoomController.java @@ -0,0 +1,58 @@ +package sumcoda.boardbuddy.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RestController; +import sumcoda.boardbuddy.dto.ChatRoomResponse; +import sumcoda.boardbuddy.dto.GatherArticleResponse; +import sumcoda.boardbuddy.dto.common.ApiResponse; +import sumcoda.boardbuddy.service.ChatRoomService; +import sumcoda.boardbuddy.service.GatherArticleService; + +import java.util.List; +import java.util.Map; + +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildSuccessResponseWithPairKeyData; + +@RestController +@RequiredArgsConstructor +public class ChatRoomController { + + private final ChatRoomService chatRoomService; + + private final GatherArticleService gatherArticleService; + + /** + * 채팅방 정보와 연관된 모집글 정보 조회 + * + * @param chatRoomId 채팅방 Id + * @param gatherArticleId 모집글 Id + * @return 채팅방과 연관된 모집글 정보 + **/ + @GetMapping("/api/chat/rooms/{chatRoomId}/gather-articles/{gatherArticleId}") + public ResponseEntity>> getChatRoomGatherArticleInfo(@PathVariable Long chatRoomId, + @PathVariable Long gatherArticleId, + @RequestAttribute String username) { + + GatherArticleResponse.SummaryInfoDTO gatherArticleSimpleInfo = gatherArticleService.getChatRoomGatherArticleSimpleInfo(chatRoomId, gatherArticleId, username); + + return buildSuccessResponseWithPairKeyData("gatherArticleSimpleInfo", gatherArticleSimpleInfo, "모집글 정보를 성공적으로 조회했습니다.", HttpStatus.OK); + } + + /** + * 특정 사용자가 참여한 채팅방 목록 조회 + * + * @param username 조회하려는 사용자의 아이디 + * @return 사용자가 참여한 채팅방 목록 + */ + @GetMapping("/api/chat/rooms") + public ResponseEntity>>> getChatRoomDetailsByUsername(@RequestAttribute String username) { + List chatRoomDetailsList = chatRoomService.getChatRoomDetailsListByUsername(username); + + return buildSuccessResponseWithPairKeyData("chatRoomDetailsList", chatRoomDetailsList, "참여중인 채팅방 목록을 성공적으로 조회했습니다.", HttpStatus.OK); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/controller/ParticipationApplicationController.java b/src/main/java/sumcoda/boardbuddy/controller/ParticipationApplicationController.java index bfe154fc..822264ce 100644 --- a/src/main/java/sumcoda/boardbuddy/controller/ParticipationApplicationController.java +++ b/src/main/java/sumcoda/boardbuddy/controller/ParticipationApplicationController.java @@ -6,6 +6,8 @@ import org.springframework.web.bind.annotation.*; import sumcoda.boardbuddy.dto.ParticipationApplicationResponse; import sumcoda.boardbuddy.dto.common.ApiResponse; +import sumcoda.boardbuddy.service.ChatMessageService; +import sumcoda.boardbuddy.service.ChatRoomService; import sumcoda.boardbuddy.service.ParticipationApplicationService; import java.util.List; @@ -19,6 +21,10 @@ public class ParticipationApplicationController { private final ParticipationApplicationService participationApplicationService; + private final ChatRoomService chatRoomService; + + private final ChatMessageService chatMessageService; + /** * 모집글 참가 신청 요청 * @@ -42,8 +48,15 @@ public ResponseEntity> applyParticipation(@PathVariable Long g **/ @PutMapping("/api/gather-articles/{gatherArticleId}/participation/{participationApplicationId}/approval") public ResponseEntity> approveParticipationApplication(@PathVariable Long gatherArticleId, @PathVariable Long participationApplicationId, @RequestAttribute String username, @RequestParam String applicantNickname) { + participationApplicationService.approveParticipationApplication(gatherArticleId, participationApplicationId, username); + // ChatRoom 입장 처리 + Long chatRoomId = chatRoomService.enterChatRoom(gatherArticleId, applicantNickname); + + // 채팅방 입장 메세지 발행 및 전송 + chatMessageService.publishEnterChatMessage(chatRoomId, applicantNickname); + return buildSuccessResponseWithoutData(applicantNickname + "님의 참가 신청을 승인 했습니다.", HttpStatus.OK); } @@ -57,6 +70,7 @@ public ResponseEntity> approveParticipationApplication(@PathVa **/ @PutMapping("/api/gather-articles/{gatherArticleId}/participation/{participationApplicationId}/rejection") public ResponseEntity> rejectParticipationApplication(@PathVariable Long gatherArticleId, @PathVariable Long participationApplicationId, @RequestAttribute String username, @RequestParam String applicantNickname) { + participationApplicationService.rejectParticipationApplication(gatherArticleId, participationApplicationId, username); return buildSuccessResponseWithoutData(applicantNickname + "님의 참가 신청을 거절 했습니다.", HttpStatus.OK); @@ -70,7 +84,17 @@ public ResponseEntity> rejectParticipationApplication(@PathVar **/ @PutMapping("/api/gather-articles/{gatherArticleId}/participation") public ResponseEntity> cancelParticipationApplication(@PathVariable Long gatherArticleId, @RequestAttribute String username) { - participationApplicationService.cancelParticipationApplication(gatherArticleId, username); + + Boolean isMemberParticipant = participationApplicationService.cancelParticipationApplication(gatherArticleId, username); + + // 만약 참가 취소하는 사용자가 참가 승인으로 인하여, 모집글에 참여중인 사용자라면, + if (isMemberParticipant) { + // ChatRoom 퇴장 처리 + Long chatRoomId = chatRoomService.leaveChatRoom(gatherArticleId, username); + + // 채팅방 퇴장 메세지 발행 및 전송 + chatMessageService.publishExitChatMessage(chatRoomId, username); + } return buildSuccessResponseWithoutData("해당 모집글의 참가 신청을 취소했습니다.", HttpStatus.OK); } @@ -84,7 +108,9 @@ public ResponseEntity> cancelParticipationApplication(@PathVar **/ @GetMapping("/api/gather-articles/{gatherArticleId}/participation") public ResponseEntity>>> getParticipationAppliedMemberList(@PathVariable Long gatherArticleId, @RequestAttribute String username) { + List participationAppliedMemberList = participationApplicationService.getParticipationAppliedMemberList(gatherArticleId, username); + return buildSuccessResponseWithPairKeyData("participationAppliedMemberList", participationAppliedMemberList, "해당 모집글의 참가 신청 목록을 성공적으로 조회했습니다.", HttpStatus.OK); } } diff --git a/src/main/java/sumcoda/boardbuddy/dto/ChatMessageRequest.java b/src/main/java/sumcoda/boardbuddy/dto/ChatMessageRequest.java new file mode 100644 index 00000000..9cc5a49f --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/dto/ChatMessageRequest.java @@ -0,0 +1,20 @@ +package sumcoda.boardbuddy.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class ChatMessageRequest { + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class PublishDTO { + private String content; + + @Builder + public PublishDTO(String content) { + this.content = content; + } + } +} diff --git a/src/main/java/sumcoda/boardbuddy/dto/ChatMessageResponse.java b/src/main/java/sumcoda/boardbuddy/dto/ChatMessageResponse.java new file mode 100644 index 00000000..8a7d6e6f --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/dto/ChatMessageResponse.java @@ -0,0 +1,72 @@ +package sumcoda.boardbuddy.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sumcoda.boardbuddy.enumerate.MessageType; + +import java.time.LocalDateTime; + +public class ChatMessageResponse { + + @Getter + @NoArgsConstructor + public static class ChatMessageInfoDTO { + + private String content; + + private String nickname; + + private String profileImageS3SavedURL; + + private Integer rank; + + private MessageType messageType; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime sentAt; + + @Builder + public ChatMessageInfoDTO(String content, String nickname, String profileImageS3SavedURL, Integer rank, MessageType messageType, LocalDateTime sentAt) { + this.content = content; + this.nickname = nickname; + this.profileImageS3SavedURL = profileImageS3SavedURL; + this.rank = rank; + this.messageType = messageType; + this.sentAt = sentAt; + } + + } + + @Getter + @NoArgsConstructor + public static class EnterOrExitMessageInfoDTO { + + private String content; + + private MessageType messageType; + + @Builder + public EnterOrExitMessageInfoDTO(String content, MessageType messageType) { + this.content = content; + this.messageType = messageType; + } + } + + @Getter + @NoArgsConstructor + public static class LatestChatMessageInfoDTO { + + private String content; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime sentAt; + + @Builder + public LatestChatMessageInfoDTO(String content, LocalDateTime sentAt) { + this.content = content; + this.sentAt = sentAt; + } + } +} diff --git a/src/main/java/sumcoda/boardbuddy/dto/ChatRoomResponse.java b/src/main/java/sumcoda/boardbuddy/dto/ChatRoomResponse.java new file mode 100644 index 00000000..5a1b1d92 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/dto/ChatRoomResponse.java @@ -0,0 +1,60 @@ +package sumcoda.boardbuddy.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sumcoda.boardbuddy.enumerate.MemberChatRoomRole; + +import java.time.LocalDateTime; + +public class ChatRoomResponse { + + @Getter + @NoArgsConstructor + public static class InfoDTO { + + private Long id; + + private LocalDateTime joinedAt; + + private MemberChatRoomRole memberChatRoomRole; + + @Builder + public InfoDTO(Long id, LocalDateTime joinedAt, MemberChatRoomRole memberChatRoomRole) { + this.id = id; + this.joinedAt = joinedAt; + this.memberChatRoomRole = memberChatRoomRole; + } + } + + @Getter + @NoArgsConstructor + public static class ValidateDTO { + + private Long id; + + @Builder + public ValidateDTO(Long id) { + this.id = id; + } + } + + @Getter + @NoArgsConstructor + public static class ChatRoomDetailsDTO { + + private Long chatRoomId; + + private GatherArticleResponse.SimpleInfoDTO gatherArticleSimpleInfo; + + private ChatMessageResponse.LatestChatMessageInfoDTO latestChatMessageInfoDTO; + + @Builder + public ChatRoomDetailsDTO(Long chatRoomId, GatherArticleResponse.SimpleInfoDTO gatherArticleSimpleInfo, ChatMessageResponse.LatestChatMessageInfoDTO latestChatMessageInfoDTO) { + this.chatRoomId = chatRoomId; + this.gatherArticleSimpleInfo = gatherArticleSimpleInfo; + this.latestChatMessageInfoDTO = latestChatMessageInfoDTO; + } + } + +} diff --git a/src/main/java/sumcoda/boardbuddy/dto/GatherArticleResponse.java b/src/main/java/sumcoda/boardbuddy/dto/GatherArticleResponse.java index 2b87024d..005bdf02 100644 --- a/src/main/java/sumcoda/boardbuddy/dto/GatherArticleResponse.java +++ b/src/main/java/sumcoda/boardbuddy/dto/GatherArticleResponse.java @@ -228,4 +228,57 @@ public ReadListDTO(List posts, Boolean last) { this.last = last; } } + + @Getter + @NoArgsConstructor + public static class SummaryInfoDTO { + + private String title; + + private String meetingLocation; + + private Integer maxParticipants; + + private Integer currentParticipants; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime startDateTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime endDateTime; + + @Builder + public SummaryInfoDTO(String title, String meetingLocation, Integer maxParticipants, Integer currentParticipants, LocalDateTime startDateTime, LocalDateTime endDateTime) { + this.title = title; + this.meetingLocation = meetingLocation; + this.maxParticipants = maxParticipants; + this.currentParticipants = currentParticipants; + this.startDateTime = startDateTime; + this.endDateTime = endDateTime; + } + } + + @Getter + @NoArgsConstructor + public static class SimpleInfoDTO { + + private Long gatherArticleId; + + private String title; + + private String meetingLocation; + + private Integer currentParticipants; + + @Builder + public SimpleInfoDTO(Long gatherArticleId, String title, String meetingLocation, Integer currentParticipants) { + this.gatherArticleId = gatherArticleId; + this.title = title; + this.meetingLocation = meetingLocation; + this.currentParticipants = currentParticipants; + } + } + + + } diff --git a/src/main/java/sumcoda/boardbuddy/dto/MemberChatRoomResponse.java b/src/main/java/sumcoda/boardbuddy/dto/MemberChatRoomResponse.java new file mode 100644 index 00000000..7700efda --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/dto/MemberChatRoomResponse.java @@ -0,0 +1,24 @@ +package sumcoda.boardbuddy.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sumcoda.boardbuddy.enumerate.MemberChatRoomRole; + +public class MemberChatRoomResponse { + + @Getter + @NoArgsConstructor + public static class ValidateDTO { + + private Long id; + + private MemberChatRoomRole memberChatRoomRole; + + @Builder + public ValidateDTO(Long id, MemberChatRoomRole memberChatRoomRole) { + this.id = id; + this.memberChatRoomRole = memberChatRoomRole; + } + } +} diff --git a/src/main/java/sumcoda/boardbuddy/entity/MemberChatRoom.java b/src/main/java/sumcoda/boardbuddy/entity/MemberChatRoom.java index d6b27a27..a1340558 100644 --- a/src/main/java/sumcoda/boardbuddy/entity/MemberChatRoom.java +++ b/src/main/java/sumcoda/boardbuddy/entity/MemberChatRoom.java @@ -5,7 +5,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import sumcoda.boardbuddy.enumerate.ChatRoomRole; +import sumcoda.boardbuddy.enumerate.MemberChatRoomRole; import java.time.LocalDateTime; @@ -25,7 +25,7 @@ public class MemberChatRoom { // 채팅 참여자의 권한을 나타내기위한 role // ex) AUTHOR, PARTICIPANT @Column(nullable = false) - private ChatRoomRole chatRoomRole; // 역할 추가 (관리자, 일반 사용자 등) + private MemberChatRoomRole memberChatRoomRole; // 역할 추가 (관리자, 일반 사용자 등) // 양방향 연관관계 // 연관관계 주인 @@ -40,18 +40,18 @@ public class MemberChatRoom { private ChatRoom chatRoom; @Builder - public MemberChatRoom(LocalDateTime joinedAt, ChatRoomRole chatRoomRole, Member member, ChatRoom chatRoom) { + public MemberChatRoom(LocalDateTime joinedAt, MemberChatRoomRole memberChatRoomRole, Member member, ChatRoom chatRoom) { this.joinedAt = joinedAt; - this.chatRoomRole = chatRoomRole; + this.memberChatRoomRole = memberChatRoomRole; this.assignMember(member); this.assignChatRoom(chatRoom); } // 직접 빌더 패턴의 생성자를 활용하지 말고 해당 메서드를 활용하여 엔티티 생성 - public static MemberChatRoom buildMemberChatRoom(LocalDateTime joinedAt, ChatRoomRole chatRoomRole, Member member, ChatRoom chatRoom) { + public static MemberChatRoom buildMemberChatRoom(LocalDateTime joinedAt, MemberChatRoomRole memberChatRoomRole, Member member, ChatRoom chatRoom) { return MemberChatRoom.builder() .joinedAt(joinedAt) - .chatRoomRole(chatRoomRole) + .memberChatRoomRole(memberChatRoomRole) .member(member) .chatRoom(chatRoom) .build(); diff --git a/src/main/java/sumcoda/boardbuddy/enumerate/ChatRoomRole.java b/src/main/java/sumcoda/boardbuddy/enumerate/MemberChatRoomRole.java similarity index 79% rename from src/main/java/sumcoda/boardbuddy/enumerate/ChatRoomRole.java rename to src/main/java/sumcoda/boardbuddy/enumerate/MemberChatRoomRole.java index 194c2683..bf9d13f7 100644 --- a/src/main/java/sumcoda/boardbuddy/enumerate/ChatRoomRole.java +++ b/src/main/java/sumcoda/boardbuddy/enumerate/MemberChatRoomRole.java @@ -5,9 +5,9 @@ @Getter @RequiredArgsConstructor -public enum ChatRoomRole { +public enum MemberChatRoomRole { - AUTHOR("author"), + HOST("host"), PARTICIPANT("participant"); private final String value; diff --git a/src/main/java/sumcoda/boardbuddy/exception/AlreadyEnteredChatRoomException.java b/src/main/java/sumcoda/boardbuddy/exception/AlreadyEnteredChatRoomException.java new file mode 100644 index 00000000..315188c8 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/AlreadyEnteredChatRoomException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class AlreadyEnteredChatRoomException extends RuntimeException { + public AlreadyEnteredChatRoomException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/ChatMessageRetrievalException.java b/src/main/java/sumcoda/boardbuddy/exception/ChatMessageRetrievalException.java new file mode 100644 index 00000000..283541bb --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/ChatMessageRetrievalException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class ChatMessageRetrievalException extends RuntimeException { + public ChatMessageRetrievalException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/ChatMessageSaveException.java b/src/main/java/sumcoda/boardbuddy/exception/ChatMessageSaveException.java new file mode 100644 index 00000000..b8f9d2fb --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/ChatMessageSaveException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class ChatMessageSaveException extends RuntimeException { + public ChatMessageSaveException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/ChatRoomAccessDeniedException.java b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomAccessDeniedException.java new file mode 100644 index 00000000..76201125 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomAccessDeniedException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class ChatRoomAccessDeniedException extends RuntimeException { + public ChatRoomAccessDeniedException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/ChatRoomHostCannotLeaveException.java b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomHostCannotLeaveException.java new file mode 100644 index 00000000..560b2918 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomHostCannotLeaveException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class ChatRoomHostCannotLeaveException extends RuntimeException { + public ChatRoomHostCannotLeaveException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/ChatRoomNotFoundException.java b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomNotFoundException.java new file mode 100644 index 00000000..635b8336 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomNotFoundException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class ChatRoomNotFoundException extends RuntimeException { + public ChatRoomNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/ChatRoomRetrievalException.java b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomRetrievalException.java new file mode 100644 index 00000000..cb2fabf9 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/ChatRoomRetrievalException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class ChatRoomRetrievalException extends RuntimeException { + public ChatRoomRetrievalException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomNotFoundException.java b/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomNotFoundException.java new file mode 100644 index 00000000..603170a7 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomNotFoundException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class MemberChatRoomNotFoundException extends RuntimeException { + public MemberChatRoomNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomRetrievalException.java b/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomRetrievalException.java new file mode 100644 index 00000000..1d75386d --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomRetrievalException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class MemberChatRoomRetrievalException extends RuntimeException { + public MemberChatRoomRetrievalException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomSaveException.java b/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomSaveException.java new file mode 100644 index 00000000..4c2d83fb --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/exception/MemberChatRoomSaveException.java @@ -0,0 +1,7 @@ +package sumcoda.boardbuddy.exception; + +public class MemberChatRoomSaveException extends RuntimeException { + public MemberChatRoomSaveException(String message) { + super(message); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/handler/exception/ChatMessageExceptionHandler.java b/src/main/java/sumcoda/boardbuddy/handler/exception/ChatMessageExceptionHandler.java new file mode 100644 index 00000000..7f9ed7aa --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/handler/exception/ChatMessageExceptionHandler.java @@ -0,0 +1,25 @@ +package sumcoda.boardbuddy.handler.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import sumcoda.boardbuddy.dto.common.ApiResponse; +import sumcoda.boardbuddy.exception.ChatMessageRetrievalException; +import sumcoda.boardbuddy.exception.ChatMessageSaveException; + +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildErrorResponse; + +@RestControllerAdvice +public class ChatMessageExceptionHandler { + + @ExceptionHandler(ChatMessageRetrievalException.class) + public ResponseEntity> handleChatMessageRetrievalException(ChatMessageRetrievalException e) { + return buildErrorResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ChatMessageSaveException.class) + public ResponseEntity> handleChatMessageSaveException(ChatMessageSaveException e) { + return buildErrorResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/handler/exception/ChatRoomExceptionHandler.java b/src/main/java/sumcoda/boardbuddy/handler/exception/ChatRoomExceptionHandler.java new file mode 100644 index 00000000..ec1ea75e --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/handler/exception/ChatRoomExceptionHandler.java @@ -0,0 +1,40 @@ +package sumcoda.boardbuddy.handler.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import sumcoda.boardbuddy.dto.common.ApiResponse; +import sumcoda.boardbuddy.exception.*; + +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildErrorResponse; +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildFailureResponse; + +@RestControllerAdvice +public class ChatRoomExceptionHandler { + + @ExceptionHandler(ChatRoomNotFoundException.class) + public ResponseEntity> handleChatRoomNotFoundException(ChatRoomNotFoundException e) { + return buildFailureResponse(e.getMessage(), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(ChatRoomHostCannotLeaveException.class) + public ResponseEntity> handleChatRoomHostCannotLeaveException(ChatRoomHostCannotLeaveException e) { + return buildFailureResponse(e.getMessage(), HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(ChatRoomRetrievalException.class) + public ResponseEntity> handleChatRoomRetrievalException(ChatRoomRetrievalException e) { + return buildErrorResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(AlreadyEnteredChatRoomException.class) + public ResponseEntity> handleAlreadyEnteredChatRoomException(AlreadyEnteredChatRoomException e) { + return buildFailureResponse(e.getMessage(), HttpStatus.CONFLICT); + } + + @ExceptionHandler(ChatRoomAccessDeniedException.class) + public ResponseEntity> handleChatRoomAccessDeniedException(ChatRoomAccessDeniedException e) { + return buildFailureResponse(e.getMessage(), HttpStatus.FORBIDDEN); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/handler/exception/MemberChatRoomExceptionHandler.java b/src/main/java/sumcoda/boardbuddy/handler/exception/MemberChatRoomExceptionHandler.java new file mode 100644 index 00000000..7e8f3402 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/handler/exception/MemberChatRoomExceptionHandler.java @@ -0,0 +1,32 @@ +package sumcoda.boardbuddy.handler.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import sumcoda.boardbuddy.dto.common.ApiResponse; +import sumcoda.boardbuddy.exception.MemberChatRoomNotFoundException; +import sumcoda.boardbuddy.exception.MemberChatRoomRetrievalException; +import sumcoda.boardbuddy.exception.MemberChatRoomSaveException; + +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildErrorResponse; +import static sumcoda.boardbuddy.builder.ResponseBuilder.buildFailureResponse; + +@RestControllerAdvice +public class MemberChatRoomExceptionHandler { + + @ExceptionHandler(MemberChatRoomNotFoundException.class) + public ResponseEntity> handleMemberChatRoomNotFoundException(MemberChatRoomNotFoundException e) { + return buildFailureResponse(e.getMessage(), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MemberChatRoomRetrievalException.class) + public ResponseEntity> handleMemberChatRoomRetrievalException(MemberChatRoomRetrievalException e) { + return buildErrorResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(MemberChatRoomSaveException.class) + public ResponseEntity> handleMemberChatRoomSaveException(MemberChatRoomSaveException e) { + return buildErrorResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepository.java b/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepository.java new file mode 100644 index 00000000..0d530c23 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepository.java @@ -0,0 +1,11 @@ +package sumcoda.boardbuddy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import sumcoda.boardbuddy.entity.ChatMessage; + +@Repository +public interface ChatMessageRepository extends JpaRepository, ChatMessageRepositoryCustom { + + +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepositoryCustom.java b/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepositoryCustom.java new file mode 100644 index 00000000..9028d742 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepositoryCustom.java @@ -0,0 +1,16 @@ +package sumcoda.boardbuddy.repository; + +import sumcoda.boardbuddy.dto.ChatMessageResponse; + +import java.util.List; +import java.util.Optional; + +public interface ChatMessageRepositoryCustom { + + List findMessagesAfterMemberJoinedByChatRoomIdAndUsername(Long chatRoomId, String username); + + Optional findTalkMessageById(Long chatMessageId); + + Optional findEnterOrExitMessageById(Long chatMessageId); + +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepositoryCustomImpl.java b/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepositoryCustomImpl.java new file mode 100644 index 00000000..63544cab --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/ChatMessageRepositoryCustomImpl.java @@ -0,0 +1,97 @@ +package sumcoda.boardbuddy.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import sumcoda.boardbuddy.dto.ChatMessageResponse; +import sumcoda.boardbuddy.exception.MemberChatRoomRetrievalException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static sumcoda.boardbuddy.entity.QChatMessage.chatMessage; +import static sumcoda.boardbuddy.entity.QChatRoom.chatRoom; +import static sumcoda.boardbuddy.entity.QMember.member; +import static sumcoda.boardbuddy.entity.QMemberChatRoom.*; +import static sumcoda.boardbuddy.entity.QProfileImage.*; + +@RequiredArgsConstructor +public class ChatMessageRepositoryCustomImpl implements ChatMessageRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + /** + * 사용자가 채팅방에 입장한 이후의 메시지 조회 + * + * @param chatRoomId 채팅방 Id + * @param username 사용자 아이디 + * @return 사용자가 입장한 이후의 채팅방 메시지 목록 + **/ + @Override + public List findMessagesAfterMemberJoinedByChatRoomIdAndUsername(Long chatRoomId, String username) { + + LocalDateTime joinedAt = jpaQueryFactory.select(memberChatRoom.joinedAt) + .from(memberChatRoom) + .where(memberChatRoom.chatRoom.id.eq(chatRoomId) + .and(memberChatRoom.member.username.eq(username))) + .fetchOne(); + + if (joinedAt == null) { + throw new MemberChatRoomRetrievalException("서버 문제로 사용자가 해당 채팅방에 입장한 시간을 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + return jpaQueryFactory.select(Projections.fields(ChatMessageResponse.ChatMessageInfoDTO.class, + chatMessage.content, + member.nickname, + profileImage.profileImageS3SavedURL, + member.rank, + chatMessage.messageType, + chatMessage.createdAt.as("sentAt"))) + .from(chatMessage) + .leftJoin(chatMessage.member, member) + .leftJoin(member.profileImage, profileImage) + .leftJoin(chatMessage.chatRoom, chatRoom) + .where(chatRoom.id.eq(chatRoomId).and(chatMessage.createdAt.after(joinedAt))) + .orderBy(chatMessage.createdAt.asc()) + .fetch(); + } + + /** + * 특정 메시지 ID에 해당하는 대화 메시지 조회 + * + * @param chatMessageId 채팅 메시지 ID + * @return 특정 메시지 ID에 해당하는 대화 메시지 정보 + **/ + @Override + public Optional findTalkMessageById(Long chatMessageId) { + return Optional.ofNullable(jpaQueryFactory.select(Projections.fields(ChatMessageResponse.ChatMessageInfoDTO.class, + chatMessage.content, + member.nickname, + profileImage.profileImageS3SavedURL, + member.rank, + chatMessage.messageType, + chatMessage.createdAt.as("sentAt"))) + .from(chatMessage) + .leftJoin(chatMessage.member, member) + .leftJoin(member.profileImage, profileImage) + .where(chatMessage.id.eq(chatMessageId)) + .fetchOne()); + } + + /** + * 특정 메시지 ID에 해당하는 입장/퇴장 메시지 조회 + * + * @param chatMessageId 채팅 메시지 ID + * @return 특정 메시지 ID에 해당하는 입장/퇴장 메시지 정보 + **/ + @Override + public Optional findEnterOrExitMessageById(Long chatMessageId) { + return Optional.ofNullable(jpaQueryFactory.select(Projections.fields(ChatMessageResponse.EnterOrExitMessageInfoDTO.class, + chatMessage.content, + chatMessage.messageType)) + .from(chatMessage) + .where(chatMessage.id.eq(chatMessageId)) + .fetchOne()); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepository.java b/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepository.java new file mode 100644 index 00000000..4e3865d7 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepository.java @@ -0,0 +1,14 @@ +package sumcoda.boardbuddy.repository.chatRoom; + +import org.springframework.data.jpa.repository.JpaRepository; +import sumcoda.boardbuddy.entity.ChatRoom; + +import java.util.Optional; + +public interface ChatRoomRepository extends JpaRepository, ChatRoomRepositoryCustom { + Optional findByGatherArticleId(Long gatherArticleId); + + Optional findById(Long chatRoomId); + + boolean existsById(Long chatRoomId); +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepositoryCustom.java b/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepositoryCustom.java new file mode 100644 index 00000000..5b405b12 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepositoryCustom.java @@ -0,0 +1,15 @@ +package sumcoda.boardbuddy.repository.chatRoom; + +import org.springframework.stereotype.Repository; +import sumcoda.boardbuddy.dto.ChatRoomResponse; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ChatRoomRepositoryCustom { + + Optional findValidateDTOByGatherArticleId(Long gatherArticleId); + + List findChatRoomDetailsListByUsername(String username); +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepositoryCustomImpl.java b/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepositoryCustomImpl.java new file mode 100644 index 00000000..904cf8b7 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/chatRoom/ChatRoomRepositoryCustomImpl.java @@ -0,0 +1,79 @@ +package sumcoda.boardbuddy.repository.chatRoom; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import sumcoda.boardbuddy.dto.ChatMessageResponse; +import sumcoda.boardbuddy.dto.ChatRoomResponse; +import sumcoda.boardbuddy.dto.GatherArticleResponse; + +import java.util.List; +import java.util.Optional; + +import static sumcoda.boardbuddy.entity.QChatMessage.chatMessage; +import static sumcoda.boardbuddy.entity.QChatRoom.*; +import static sumcoda.boardbuddy.entity.QGatherArticle.gatherArticle; +import static sumcoda.boardbuddy.entity.QMember.member; +import static sumcoda.boardbuddy.entity.QMemberChatRoom.memberChatRoom; + +@RequiredArgsConstructor +public class ChatRoomRepositoryCustomImpl implements ChatRoomRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + + /** + * 특정 모집글 Id로 ChatRoom 검증 정보 조회 + * + * @param gatherArticleId 모집글 Id + * @return ChatRoom 검증 정보가 포함된 ValidateDTO 객체 + **/ + @Override + public Optional findValidateDTOByGatherArticleId(Long gatherArticleId) { + return Optional.ofNullable(jpaQueryFactory.select(Projections.fields(ChatRoomResponse.ValidateDTO.class, + chatRoom.id)) + .from(chatRoom) + .join(chatRoom.gatherArticle, gatherArticle) + .where(gatherArticle.id.eq(gatherArticleId)) + .fetchOne()); + } + + /** + * 특정 사용자 아이디 사용자가 속한 채팅방 상세 정보 목록 조회 + * + * @param username 사용자 아이디 + * @return 사용자가 속한 채팅방의 상세 정보 목록 + **/ + @Override + public List findChatRoomDetailsListByUsername(String username) { + return jpaQueryFactory + .select(Projections.fields(ChatRoomResponse.ChatRoomDetailsDTO.class, + chatRoom.id, + Projections.constructor(GatherArticleResponse.SimpleInfoDTO.class, + gatherArticle.id, + gatherArticle.title, + gatherArticle.meetingLocation, + gatherArticle.currentParticipants + ).as("gatherArticleSimpleInfo"), + Projections.fields(ChatMessageResponse.LatestChatMessageInfoDTO.class, + chatMessage.content, + chatMessage.createdAt.as("sentAt") + ) + )) + .from(chatRoom) + .leftJoin(chatRoom.chatMessages, chatMessage) + .join(chatRoom.gatherArticle, gatherArticle) + .join(chatRoom.memberChatRooms, memberChatRoom) + .join(memberChatRoom.member, member) + .where(member.username.eq(username) + .and(chatMessage.createdAt.eq( + JPAExpressions + .select(chatMessage.createdAt.max()) + .from(chatMessage) + .where(chatMessage.chatRoom.id.eq(chatRoom.id)) + )) + ) + .fetch(); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepository.java b/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepository.java index 9b9fd54f..0d677065 100644 --- a/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepository.java +++ b/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepository.java @@ -12,4 +12,6 @@ public interface GatherArticleRepository extends JpaRepository findById(Long gatherArticleId); boolean existsById(Long gatherArticleId); + + Boolean existsByChatRoomIdAndId(Long chatRoomId, Long gatherArticleId); } \ No newline at end of file diff --git a/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustom.java b/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustom.java index 7b1441d7..8b2f2456 100644 --- a/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustom.java +++ b/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustom.java @@ -21,7 +21,10 @@ public interface GatherArticleRepositoryCustom { Optional findIdDTOById(Long gatherArticleId); Slice findReadSliceDTOByLocationAndStatusAndSort( - List sidoList, List sggList, List emdList, String status, String sort, Pageable pageable); + + List sidoList, List siguList, List dongList, String status, String sort, Pageable pageable); + + Optional findSimpleInfoByGatherArticleId(Long gatherArticleId); GatherArticleResponse.ReadDTO findGatherArticleReadDTOByGatherArticleId(Long gatherArticleId, Long memberId); } \ No newline at end of file diff --git a/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustomImpl.java b/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustomImpl.java index a9dd8153..6d48e720 100644 --- a/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustomImpl.java +++ b/src/main/java/sumcoda/boardbuddy/repository/gatherArticle/GatherArticleRepositoryCustomImpl.java @@ -152,6 +152,28 @@ public Slice findReadSliceDTOByLocationAndSt return new SliceImpl<>(results, pageable, hasNext); } + /** + * 특정 모집글 Id로 간단한 모집글 정보 조회 + * + * @param gatherArticleId 모집글 Id + * @return 모집글 정보가 포함된 SummaryInfoDTO 객체 + **/ + @Override + public Optional findSimpleInfoByGatherArticleId(Long gatherArticleId) { + return Optional.ofNullable(jpaQueryFactory + .select(Projections.fields(GatherArticleResponse.SummaryInfoDTO.class, + gatherArticle.title, + gatherArticle.meetingLocation, + gatherArticle.maxParticipants, + gatherArticle.currentParticipants, + gatherArticle.startDateTime, + gatherArticle.endDateTime + )) + .from(gatherArticle) + .where(gatherArticle.id.eq(gatherArticleId)) + .fetchOne()); + } + private BooleanExpression eqStatus(String status) { return status != null ? gatherArticle.gatherArticleStatus.eq(GatherArticleStatus.valueOf(status.toUpperCase())) : null; } diff --git a/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepository.java b/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepository.java new file mode 100644 index 00000000..090aa969 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepository.java @@ -0,0 +1,13 @@ +package sumcoda.boardbuddy.repository.memberChatRoom; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import sumcoda.boardbuddy.entity.MemberChatRoom; + +@Repository +public interface MemberChatRoomRepository extends JpaRepository, MemberChatRoomRepositoryCustom { + + Boolean existsByChatRoomIdAndMemberUsername(Long chatRoomId, String username); + + Boolean existsByChatRoomIdAndMemberNickname(Long chatRoomId, String nickname); +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepositoryCustom.java b/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepositoryCustom.java new file mode 100644 index 00000000..17e40038 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepositoryCustom.java @@ -0,0 +1,12 @@ +package sumcoda.boardbuddy.repository.memberChatRoom; + +import sumcoda.boardbuddy.dto.MemberChatRoomResponse; + +import java.util.Optional; + +public interface MemberChatRoomRepositoryCustom { + + Boolean existsByGatherArticleIdAndNickname(Long gatherArticleId, String nickname); + + Optional findByGatherArticleIdAndUsername(Long gatherArticleId, String username); +} diff --git a/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepositoryCustomImpl.java b/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepositoryCustomImpl.java new file mode 100644 index 00000000..2a940d15 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/repository/memberChatRoom/MemberChatRoomRepositoryCustomImpl.java @@ -0,0 +1,62 @@ +package sumcoda.boardbuddy.repository.memberChatRoom; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import sumcoda.boardbuddy.dto.MemberChatRoomResponse; + +import java.util.Optional; + +import static sumcoda.boardbuddy.entity.QChatRoom.chatRoom; +import static sumcoda.boardbuddy.entity.QGatherArticle.gatherArticle; +import static sumcoda.boardbuddy.entity.QMember.member; +import static sumcoda.boardbuddy.entity.QMemberChatRoom.*; + +@RequiredArgsConstructor +public class MemberChatRoomRepositoryCustomImpl implements MemberChatRoomRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + /** + * 특정 모집글 Id와 닉네임을 가진 사용자가 존재하는지 확인 + * + * @param gatherArticleId 모집글 Id + * @param nickname 사용자 닉네임 + * @return 사용자가 존재하면 true, 아니면 false + **/ + @Override + public Boolean existsByGatherArticleIdAndNickname(Long gatherArticleId, String nickname) { + return jpaQueryFactory + .selectOne() + .from(memberChatRoom) + .leftJoin(memberChatRoom.member, member) + .leftJoin(memberChatRoom.chatRoom, chatRoom) + .leftJoin(chatRoom.gatherArticle, gatherArticle) + .where(gatherArticle.id.eq(gatherArticleId) + .and(member.nickname.eq(nickname))) + .fetchOne() != null; + } + + /** + * 특정 모집글 Id와 사용자 아이디로 MemberChatRoom 정보를 조회 + * + * @param gatherArticleId 모집글 Id + * @param username 사용자 아이디 + * @return MemberChatRoom 정보가 포함된 ValidateDTO 객체 + **/ + @Override + public Optional findByGatherArticleIdAndUsername(Long gatherArticleId, String username) { + return Optional.ofNullable(jpaQueryFactory + .select(Projections.fields(MemberChatRoomResponse.ValidateDTO.class, + memberChatRoom.id, + memberChatRoom.memberChatRoomRole + )) + .from(memberChatRoom) + .leftJoin(memberChatRoom.chatRoom, chatRoom) + .leftJoin(memberChatRoom.member, member) + .leftJoin(chatRoom.gatherArticle, gatherArticle) + .where(gatherArticle.id.eq(gatherArticleId) + .and(member.username.eq(username))) + .fetchOne()); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/service/ChatMessageService.java b/src/main/java/sumcoda/boardbuddy/service/ChatMessageService.java new file mode 100644 index 00000000..7ada9063 --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/service/ChatMessageService.java @@ -0,0 +1,197 @@ +package sumcoda.boardbuddy.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sumcoda.boardbuddy.dto.ChatMessageRequest; +import sumcoda.boardbuddy.dto.ChatMessageResponse; +import sumcoda.boardbuddy.entity.ChatMessage; +import sumcoda.boardbuddy.entity.ChatRoom; +import sumcoda.boardbuddy.entity.Member; +import sumcoda.boardbuddy.enumerate.MessageType; +import sumcoda.boardbuddy.exception.*; +import sumcoda.boardbuddy.exception.member.MemberNotFoundException; +import sumcoda.boardbuddy.exception.member.MemberRetrievalException; +import sumcoda.boardbuddy.repository.ChatMessageRepository; +import sumcoda.boardbuddy.repository.chatRoom.ChatRoomRepository; +import sumcoda.boardbuddy.repository.MemberRepository; +import sumcoda.boardbuddy.repository.memberChatRoom.MemberChatRoomRepository; +import sumcoda.boardbuddy.util.ChatMessageUtil; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatMessageService { + + private final ChatMessageRepository chatMessageRepository; + + private final ChatRoomRepository chatRoomRepository; + + private final MemberRepository memberRepository; + + private final MemberChatRoomRepository memberChatRoomRepository; + + private final SimpMessagingTemplate messagingTemplate; + + /** + * 메세지 발행 및 채팅방에 메세지 전송 + * + * @param chatRoomId 채팅방 Id + * @param publishDTO 전송할 메시지 내용 + * @param username 메세지를 전송하는 사용자 아이디 + **/ + @Transactional + public void publishMessage(Long chatRoomId, ChatMessageRequest.PublishDTO publishDTO, String username) { + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new ChatRoomNotFoundException("해당 채팅방이 존재하지 않습니다.")); + + Member member = memberRepository.findByUsername(username) + .orElseThrow(() -> new MemberNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + Boolean isMemberChatRoomExists = memberChatRoomRepository.existsByChatRoomIdAndMemberUsername(chatRoomId, username); + if (!isMemberChatRoomExists) { + throw new MemberChatRoomRetrievalException("서버 문제로 해당 채팅방의 사용자 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + String content = publishDTO.getContent(); + + ChatMessage chatMessage = ChatMessage.buildChatMessage(content, MessageType.TALK, member, chatRoom); + + Long chatMessageId = chatMessageRepository.save(chatMessage).getId(); + + if (chatMessageId == null) { + throw new ChatMessageSaveException("서버 문제로 메세지를 저장할 수 없습니다. 관리자에게 문의하세요."); + } + + ChatMessageResponse.ChatMessageInfoDTO responseChatMessage = chatMessageRepository.findTalkMessageById(chatMessageId) + .orElseThrow(() -> new ChatMessageRetrievalException("서버 문제로 해당 메세지를 찾을 수 없습니다. 관리자에게 문의하세요.")); + + messagingTemplate.convertAndSend("/api/chat/reception/" + chatRoomId, responseChatMessage); + } + + /** + * 채팅방 입장 메세지 발행 및채팅방에 사용자 입장 메세지 전송 + * + * @param chatRoomId 채팅방 Id + * @param nickname 입장하는 사용자 닉네임 + **/ + @Transactional + public void publishEnterChatMessage(Long chatRoomId, String nickname) { + + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new ChatRoomNotFoundException("해당 채팅방이 존재하지 않습니다.")); + + Member member = memberRepository.findByNickname(nickname) + .orElseThrow(() -> new MemberRetrievalException("해당 유저를 찾을 수 없습니다. 관리자에게 문의하세요.")); + + Boolean isMemberChatRoomExists = memberChatRoomRepository.existsByChatRoomIdAndMemberNickname(chatRoomId, nickname); + if (!isMemberChatRoomExists) { + throw new MemberChatRoomRetrievalException("서버 문제로 해당 채팅방의 사용자 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + publishEnterOrExitChatMessage(MessageType.ENTER, member, chatRoom); + } + + /** + * 채팅방 퇴장 메세지 발행 및 채팅방에 사용자 퇴장 메세지 전송 + * + * @param chatRoomId 채팅방 Id + * @param username 퇴장하는 사용자 아이디 + **/ + @Transactional + public void publishExitChatMessage(Long chatRoomId, String username) { + + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new ChatRoomNotFoundException("해당 채팅방이 존재하지 않습니다.")); + + Member member = memberRepository.findByUsername(username) + .orElseThrow(() -> new MemberRetrievalException("해당 유저를 찾을 수 없습니다. 관리자에게 문의하세요.")); + + Boolean isMemberChatRoomExists = memberChatRoomRepository.existsByChatRoomIdAndMemberUsername(chatRoomId, username); + if (!isMemberChatRoomExists) { + throw new MemberChatRoomRetrievalException("서버 문제로 해당 채팅방의 사용자 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + publishEnterOrExitChatMessage(MessageType.EXIT, member, chatRoom); + } + + /** + * 채팅방 입장/퇴장 메세지 발행 및 채팅방에 사용자 입장/퇴장 메세지 전송 + * + * @param messageType 메세지 유형 (입장/퇴장) + * @param member 사용자 정보 + * @param chatRoom 채팅방 정보 + **/ + private void publishEnterOrExitChatMessage(MessageType messageType, Member member, ChatRoom chatRoom) { + + Long chatRoomId = chatRoom.getId(); + + String nickname = member.getNickname(); + + String content = ChatMessageUtil.buildChatMessageContent(nickname, messageType); + + ChatMessage chatMessage = ChatMessage.buildChatMessage(content, messageType, member, chatRoom); + + Long chatMessageId = chatMessageRepository.save(chatMessage).getId(); + + if (chatMessageId == null) { + throw new ChatMessageSaveException("서버 문제로 메세지를 저장할 수 없습니다. 관리자에게 문의하세요."); + } + + ChatMessageResponse.EnterOrExitMessageInfoDTO responseChatMessage = chatMessageRepository.findEnterOrExitMessageById(chatMessageId) + .orElseThrow(() -> new ChatMessageRetrievalException("서버 문제로 해당 메세지를 찾을 수 없습니다. 관리자에게 문의하세요.")); + + // 채팅방 구독자들에게 메시지 전송 + messagingTemplate.convertAndSend("/api/chat/reception/" + chatRoomId, responseChatMessage); + } + + /** + * 사용자가 채팅방에 입장한 이후의 메세지 조회 + * + * @param chatRoomId 채팅방 Id + * @param username 사용자 아이디 + * @return 사용자가 입장한 이후의 채팅방 메시지 목록 + **/ + public List findMessagesAfterMemberJoinedByChatRoomIdAndUsername(Long chatRoomId, String username) { + + boolean isChatRoomExists = chatRoomRepository.existsById(chatRoomId); + + if (!isChatRoomExists) { + throw new ChatRoomNotFoundException("입장하려는 채팅방을 찾을 수 없습니다."); + } + + Boolean isMemberChatRoomExists = memberChatRoomRepository.existsByChatRoomIdAndMemberUsername(chatRoomId, username); + if (isMemberChatRoomExists) { + throw new ChatRoomAccessDeniedException("해당 채팅방에 입장하지 않은 사용자입니다."); + } + + // 채팅방 메시지 조회 로직 + List messages = chatMessageRepository.findMessagesAfterMemberJoinedByChatRoomIdAndUsername(chatRoomId, username); + + if (messages.isEmpty()) { + throw new ChatMessageRetrievalException("서버 문제로 메시지를 조회하지 못하였습니다. 관리자에게 문의하세요"); + } + + return messages.stream() + .map(message -> { + ChatMessageResponse.ChatMessageInfoDTO.ChatMessageInfoDTOBuilder builder = + ChatMessageResponse.ChatMessageInfoDTO.builder() + .content(message.getContent()) + .messageType(message.getMessageType()); + + if (message.getMessageType() == MessageType.TALK) { + builder.nickname(message.getNickname()) + .profileImageS3SavedURL(message.getProfileImageS3SavedURL()) + .rank(message.getRank()) + .sentAt(message.getSentAt()); + } + + return builder.build(); + }) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/sumcoda/boardbuddy/service/ChatRoomService.java b/src/main/java/sumcoda/boardbuddy/service/ChatRoomService.java new file mode 100644 index 00000000..eceaa6fe --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/service/ChatRoomService.java @@ -0,0 +1,122 @@ +package sumcoda.boardbuddy.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sumcoda.boardbuddy.dto.ChatRoomResponse; +import sumcoda.boardbuddy.dto.MemberChatRoomResponse; +import sumcoda.boardbuddy.entity.ChatRoom; +import sumcoda.boardbuddy.entity.Member; +import sumcoda.boardbuddy.entity.MemberChatRoom; +import sumcoda.boardbuddy.enumerate.MemberChatRoomRole; +import sumcoda.boardbuddy.exception.*; +import sumcoda.boardbuddy.exception.member.MemberNotFoundException; +import sumcoda.boardbuddy.exception.member.MemberRetrievalException; +import sumcoda.boardbuddy.repository.chatRoom.ChatRoomRepository; +import sumcoda.boardbuddy.repository.memberChatRoom.MemberChatRoomRepository; +import sumcoda.boardbuddy.repository.MemberRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatRoomService { + + private final ChatRoomRepository chatRoomRepository; + + private final MemberChatRoomRepository memberChatRoomRepository; + + private final MemberRepository memberRepository; + + /** + * 사용자가 특정 모집글과 관련된 채팅방에 입장 + * + * @param gatherArticleId 모집글 Id + * @param nickname 사용자 닉네임 + **/ + @Transactional + public Long enterChatRoom(Long gatherArticleId, String nickname) { + ChatRoom chatRoom = chatRoomRepository.findByGatherArticleId(gatherArticleId) + .orElseThrow(() -> new ChatRoomNotFoundException("해당 모집글에 대한 채팅방이 존재하지 않습니다.")); + + Long chatRoomId = chatRoom.getId(); + + if (chatRoomId == null) { + throw new ChatRoomRetrievalException("서버 문제로 해당 채팅방의 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + Member member = memberRepository.findByNickname(nickname) + .orElseThrow(() -> new MemberNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + Boolean isMemberChatRoomExists = memberChatRoomRepository.existsByGatherArticleIdAndNickname(gatherArticleId, nickname); + if (isMemberChatRoomExists) { + throw new AlreadyEnteredChatRoomException("해당 채팅방은 이미 입장한 채팅방입니다."); + } + + MemberChatRoom memberChatRoom = MemberChatRoom.buildMemberChatRoom(LocalDateTime.now(), MemberChatRoomRole.PARTICIPANT, member, chatRoom); + + Long memberChatRoomId = memberChatRoomRepository.save(memberChatRoom).getId(); + + if (memberChatRoomId == null) { + throw new MemberChatRoomSaveException("서버 문제로 채팅방 관련 사용자의 정보를 저장하지 못했습니다. 관리자에게 문의하세요."); + } + + return chatRoomId; + } + + /** + * 사용자가 특정 모집글과 관련된 채팅방에서 퇴장 + * + * @param gatherArticleId 모집글 Id + * @param username 사용자 아이디 + **/ + @Transactional + public Long leaveChatRoom(Long gatherArticleId, String username) { + ChatRoomResponse.ValidateDTO chatRoomValidateDTO = chatRoomRepository.findValidateDTOByGatherArticleId(gatherArticleId) + .orElseThrow(() -> new ChatRoomNotFoundException("해당 모집글에 대한 채팅방이 존재하지 않습니다")); + + Long chatRoomId = chatRoomValidateDTO.getId(); + if (chatRoomId == null) { + throw new ChatRoomRetrievalException("서버문제로 해당 모집글에 대한 채팅방 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + MemberChatRoomResponse.ValidateDTO memberChatRoomValidateDTO = memberChatRoomRepository.findByGatherArticleIdAndUsername(gatherArticleId, username) + .orElseThrow(() -> new MemberChatRoomNotFoundException("채팅방 관련 사용자의 정보를 찾을 수 없습니다.")); + + MemberChatRoomRole memberChatRoomRole = memberChatRoomValidateDTO.getMemberChatRoomRole(); + if (memberChatRoomRole == null) { + throw new MemberChatRoomRetrievalException("서버 문제로 채팅방 관련 사용자의 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + if (memberChatRoomValidateDTO.getMemberChatRoomRole() == MemberChatRoomRole.HOST) { + throw new ChatRoomHostCannotLeaveException("채팅방의 방장은 채팅방을 퇴장할 수 없습니다."); + } + + Long memberChatRoomId = memberChatRoomValidateDTO.getId(); + + if (memberChatRoomId == null) { + throw new MemberChatRoomRetrievalException("서버 문제로 채팅방 관련 사용자의 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + memberChatRoomRepository.deleteById(memberChatRoomId); + + return chatRoomId; + } + + /** + * 특정 사용자가 참여하고 있는 채팅방 상세 정보 목록 조회 + * + * @param username 사용자 아이디 + * @return 사용자가 참여하고 있는 채팅방 상세 정보 목록 + **/ + public List getChatRoomDetailsListByUsername(String username) { + Boolean isMemberExists = memberRepository.existsByUsername(username); + if (!isMemberExists) { + throw new MemberRetrievalException("서버 문제로 사용자의 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + return chatRoomRepository.findChatRoomDetailsListByUsername(username); + } +} \ No newline at end of file diff --git a/src/main/java/sumcoda/boardbuddy/service/GatherArticleService.java b/src/main/java/sumcoda/boardbuddy/service/GatherArticleService.java index 7bf709bc..76c7bcc6 100644 --- a/src/main/java/sumcoda/boardbuddy/service/GatherArticleService.java +++ b/src/main/java/sumcoda/boardbuddy/service/GatherArticleService.java @@ -19,6 +19,7 @@ import sumcoda.boardbuddy.enumerate.GatherArticleStatus; import sumcoda.boardbuddy.exception.gatherArticle.*; import sumcoda.boardbuddy.exception.member.MemberRetrievalException; +import sumcoda.boardbuddy.exception.memberGatherArticle.MemberGatherArticleRetrievalException; import sumcoda.boardbuddy.exception.nearPublicDistrict.NearPublicDistrictRetrievalException; import sumcoda.boardbuddy.exception.publicDistrict.PublicDistrictRetrievalException; import sumcoda.boardbuddy.repository.gatherArticle.GatherArticleRepository; @@ -396,4 +397,31 @@ public GatherArticleResponse.ReadListDTO getGatherArticles(Integer page, String .last(readSliceDTO.isLast()) .build(); } + + /** + * 채팅방 정보와 연관된 모집글 간단 정보 조회 + * + * @param chatRoomId 채팅방 Id + * @param gatherArticleId 모집글 Id + * @param username 사용자 아이디 + * @return 채팅방과 연관된 모집글 간단 정보 + **/ + public GatherArticleResponse.SummaryInfoDTO getChatRoomGatherArticleSimpleInfo(Long chatRoomId, Long gatherArticleId, String username) { + + // 모집글 정보 조회 + boolean isGatherArticleExists = gatherArticleRepository.existsByChatRoomIdAndId(chatRoomId, gatherArticleId); + + if (isGatherArticleExists) { + throw new GatherArticleNotFoundException("모집글 정보를 찾을 수 없습니다."); + } + + Boolean isMemberGatherArticleExists = memberGatherArticleRepository.existsByGatherArticleIdAndMemberUsername(gatherArticleId, username); + + if (!isMemberGatherArticleExists) { + throw new MemberGatherArticleRetrievalException("서버 문제로 해당 모집글 관련 사용자의 정보를 찾을 수 없습니다. 관리자에게 문의하세요."); + } + + return gatherArticleRepository.findSimpleInfoByGatherArticleId(gatherArticleId) + .orElseThrow(() -> new GatherArticleRetrievalException("서버 문제로 해당 모집글에 대한 정보를 찾을 수 없습니다. 관리자에게 문의하세요.")); + } } diff --git a/src/main/java/sumcoda/boardbuddy/service/ParticipationApplicationService.java b/src/main/java/sumcoda/boardbuddy/service/ParticipationApplicationService.java index a0238666..b956c2e4 100644 --- a/src/main/java/sumcoda/boardbuddy/service/ParticipationApplicationService.java +++ b/src/main/java/sumcoda/boardbuddy/service/ParticipationApplicationService.java @@ -228,7 +228,7 @@ public void rejectParticipationApplication(Long gatherArticleId, Long participat * @param username 참가신청을 취소하는 사용자 아이디 **/ @Transactional - public void cancelParticipationApplication(Long gatherArticleId, String username) { + public Boolean cancelParticipationApplication(Long gatherArticleId, String username) { GatherArticle gatherArticle = gatherArticleRepository.findById(gatherArticleId) .orElseThrow(() -> new GatherArticleNotFoundException("해당 모집글이 존재하지 않습니다.")); @@ -258,12 +258,17 @@ public void cancelParticipationApplication(Long gatherArticleId, String username // 참가 신청 취소 처리 participationApplication.assignParticipationApplicationStatus(ParticipationApplicationStatus.CANCELED); + boolean isMemberParticipant = false; + if (memberGatherArticle.getMemberGatherArticleRole() == MemberGatherArticleRole.PARTICIPANT) { + isMemberParticipant = true; memberGatherArticle.assignMemberGatherArticleRole(MemberGatherArticleRole.NONE); } // 모집글의 현재 참가자 수 업데이트 gatherArticle.assignCurrentParticipants(gatherArticle.getCurrentParticipants() - 1); + + return isMemberParticipant; } /** diff --git a/src/main/java/sumcoda/boardbuddy/util/ChatMessageUtil.java b/src/main/java/sumcoda/boardbuddy/util/ChatMessageUtil.java new file mode 100644 index 00000000..80ba9d0c --- /dev/null +++ b/src/main/java/sumcoda/boardbuddy/util/ChatMessageUtil.java @@ -0,0 +1,25 @@ +package sumcoda.boardbuddy.util; + +import sumcoda.boardbuddy.enumerate.MessageType; + +public class ChatMessageUtil { + + /** + * 채팅방 입장/퇴장 메시지 내용을 생성 + * + * @param nickname 사용자 닉네임 + * @param messageType 메시지 유형 (입장/퇴장) + * @return 생성된 채팅 메시지 내용 + **/ + public static String buildChatMessageContent(String nickname, MessageType messageType) { + String content = ""; + + if (messageType.equals(MessageType.ENTER)) { + content = "[입장] " + nickname + "님이 채팅방에 입장했습니다"; + } else if (messageType.equals(MessageType.EXIT)) { + content = "[퇴장] " + nickname + "님이 채팅방에서 퇴장했습니다."; + } + + return content; + } +}