From 069b86dc2719e2f115870044bd619ea5ea3b2dde Mon Sep 17 00:00:00 2001 From: Son Gahyun <77109954+hyun2371@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:05:36 +0900 Subject: [PATCH] =?UTF-8?q?[feat=20#142]=20=EC=B1=84=ED=8C=85=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20API=EB=A5=BC=20=EC=B1=84=ED=8C=85=EB=B0=A9=20API?= =?UTF-8?q?=EB=A1=9C=EB=B6=80=ED=84=B0=20=EB=B6=84=EB=A6=AC=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [refactor] : 채팅 상대 조회 메서드 리팩토링 * [feat] : 채팅방 엔티티에 채팅방 상태 필드 삭제 * [feat] : 채팅방 상태 필드 -> 채팅 요청 상태 필드로 변경 * [feat] : 채팅방 상태 필드 -> 채팅 요청 상태 필드로 변경 반영 * [test] : 채팅방 상태 필드 -> 채팅 요청 상태 필드로 변경 테스트 반영 * [feat] : 채팅 요청 엔티티 추가 * [rename] : chat 디렉토리 이름 chatroom으로 변경 * [rename] : 채팅요청 관련 도메인 이동 * [feat] : 채팅요청 조회 응답 생성 * [feat] : 채팅 요청 관련 repository 함수 추가 * [test] : 채팅 요청 repository 동적 쿼리 테스트 * [feat] : 채팅 요청 생성 관련 dto 추가 * [feat] : 채팅 요청 dto mapper 추가 * [feat] : 채팅 요청 생성 API 메서드 추가 * [feat] : 채팅 요청 수락 및 거절 비즈니스 로직 추가 * [feat] : 채팅 요청 수락 및 거절 API 메서드 추가 * [feat] : 채팅 요청 목록 조회 비즈니스 로직 추가 * [feat] : 채팅 요청 목록 조회 API 메서드 추가 * [feat] : 채팅 요청 수락 거절 관련 mapper 추가 * [feat] : 채팅 요청 거절 스케줄러 추가 * [test] : 채팅 요청 비즈니스 로직 단위 테스트 * [test] : 채팅 요청 API 통합 테스트 * [feat] : 채팅방 -> 채팅요청 repository로 옮긴 메서드 삭제 * [test] : 채팅방 -> 채팅요청 repository로 옮긴 메서드 삭제 테스트 반영 * [test] : chatRoomFixture 불필요한 메서드 및 필드 제거 * [feat] : chatroom 불필요한 비즈니스 메서드 제거 * [feat] : 불필요한 dto 삭제 * [test] : 채팅방 비즈니스 테스트 불필요한 로직 삭제 * [feat] : 채팅방 API에 있던 요청 관련 API 메서드 삭제 * [test] : 채팅방 API에 있던 요청 관련 API 메서드 삭제 반영 * [style] : 코드 리포멧팅 * [refactor] : dto 필드명 변경 (chatStatus-> inquiryStatus) * [test] : dto 필드명 변경 반영 * [refactor] : 불필요한 DTO 삭제 * [feat] : dto - 채팅 상대 정보 memberInfo 객체 사용 * [feat] : 채팅 목록 조회 dto - 채팅 상대 정보 memberInfo 객체 사용 * [test] : 채팅 목록 조회 dto - 채팅 상대 정보 memberInfo 객체 사용 테스트 반영 * [refactor] : 요청 dto 필드명 변경 * [test] : chatInquiry fixture 생성 * [test] : 채팅 요청 생성 비즈니스 로직 테스트 * [feat] : 채팅 요청 API uri 및 메서드 수정 * [fix] : controller에 누락된 requestBody 어노테이션 추가 * [test] : 채팅 요청 생성 통합 테스트 * [fix] : 채팅 메시지 null일 때 NPE 해결 --- .../chat/controller/ChatRoomController.java | 109 ------- .../dnd/gongmuin/chat/dto/ChatRoomMapper.java | 127 -------- .../chat/dto/response/AcceptChatResponse.java | 7 - .../chat/dto/response/ChatProposalInfo.java | 36 --- .../dto/response/ChatProposalResponse.java | 13 - .../dto/response/CreateChatRoomResponse.java | 13 - .../chat/dto/response/RejectChatResponse.java | 6 - .../repository/ChatRoomQueryRepository.java | 21 -- .../ChatRoomQueryRepositoryImpl.java | 129 -------- .../chat/service/ChatRoomService.java | 249 --------------- .../controller/ChatInquiryController.java | 75 +++++ .../domain/ChatInquiry.java} | 34 +- .../domain/InquiryStatus.java} | 8 +- .../chat_inquiry/dto/AcceptChatResponse.java | 8 + .../chat_inquiry/dto/ChatInquiryMapper.java | 63 ++++ .../chat_inquiry/dto/ChatInquiryResponse.java | 39 +++ .../dto/CreateChatInquiryRequest.java | 15 + .../dto/CreateChatInquiryResponse.java | 11 + .../chat_inquiry/dto/RejectChatResponse.java | 6 + .../ChatInquiryQueryRepository.java | 17 + .../ChatInquiryQueryRepositoryImpl.java | 92 ++++++ .../repository/ChatInquiryRepository.java | 8 + .../scheduler/ChatInquiryScheduler.java} | 13 +- .../service/ChatInquiryService.java | 139 ++++++++ .../controller/ChatMessageController.java | 8 +- .../controller/ChatRoomController.java | 60 ++++ .../domain/ChatMessage.java | 2 +- .../gongmuin/chatroom/domain/ChatRoom.java | 61 ++++ .../domain/MessageType.java | 4 +- .../dto/ChatMessageMapper.java | 12 +- .../gongmuin/chatroom/dto/ChatRoomMapper.java | 77 +++++ .../dto/request/ChatMessageRequest.java | 2 +- .../dto/request/CreateChatRoomRequest.java | 2 +- .../dto/response/ChatMessageResponse.java | 2 +- .../dto/response/ChatRoomDetailResponse.java | 3 +- .../dto/response/ChatRoomInfo.java | 2 +- .../dto/response/ChatRoomSimpleResponse.java | 2 +- .../dto/response/LatestChatMessage.java | 2 +- .../exception/ChatErrorCode.java | 2 +- .../ChatMessageQueryRepository.java | 4 +- .../repository/ChatMessageRepository.java | 4 +- .../repository/ChatRoomQueryRepository.java | 12 + .../ChatRoomQueryRepositoryImpl.java | 65 ++++ .../repository/ChatRoomRepository.java | 4 +- .../service/ChatMessageService.java | 12 +- .../chatroom/service/ChatRoomService.java | 113 +++++++ .../dnd/gongmuin/member/domain/Member.java | 4 + .../controller/ChatRoomControllerTest.java | 122 +------ .../ChatMessageQueryRepositoryTest.java | 4 +- .../repository/ChatRoomRepositoryTest.java | 73 +---- .../chat/service/ChatRoomServiceTest.java | 299 +----------------- .../controller/ChatInquiryControllerTest.java | 148 +++++++++ .../repository/ChatInquiryRepositoryTest.java | 101 ++++++ .../service/ChatInquiryServiceTest.java | 283 +++++++++++++++++ .../common/fixture/ChatInquiryFixture.java | 45 +++ .../common/fixture/ChatMessageFixture.java | 4 +- .../common/fixture/ChatRoomFixture.java | 17 +- 57 files changed, 1537 insertions(+), 1256 deletions(-) delete mode 100644 src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/dto/ChatRoomMapper.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/dto/response/AcceptChatResponse.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalInfo.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalResponse.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/dto/response/CreateChatRoomResponse.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/dto/response/RejectChatResponse.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepository.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepositoryImpl.java delete mode 100644 src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryController.java rename src/main/java/com/dnd/gongmuin/{chat/domain/ChatRoom.java => chat_inquiry/domain/ChatInquiry.java} (72%) rename src/main/java/com/dnd/gongmuin/{chat/domain/ChatStatus.java => chat_inquiry/domain/InquiryStatus.java} (75%) create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/dto/AcceptChatResponse.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryMapper.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryRequest.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryResponse.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/dto/RejectChatResponse.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepository.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepositoryImpl.java create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepository.java rename src/main/java/com/dnd/gongmuin/{chat/scheduler/ChatScheduler.java => chat_inquiry/scheduler/ChatInquiryScheduler.java} (54%) create mode 100644 src/main/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryService.java rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/controller/ChatMessageController.java (76%) create mode 100644 src/main/java/com/dnd/gongmuin/chatroom/controller/ChatRoomController.java rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/domain/ChatMessage.java (95%) create mode 100644 src/main/java/com/dnd/gongmuin/chatroom/domain/ChatRoom.java rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/domain/MessageType.java (86%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/ChatMessageMapper.java (76%) create mode 100644 src/main/java/com/dnd/gongmuin/chatroom/dto/ChatRoomMapper.java rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/request/ChatMessageRequest.java (88%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/request/CreateChatRoomRequest.java (86%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/response/ChatMessageResponse.java (72%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/response/ChatRoomDetailResponse.java (77%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/response/ChatRoomInfo.java (91%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/response/ChatRoomSimpleResponse.java (82%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/dto/response/LatestChatMessage.java (75%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/exception/ChatErrorCode.java (94%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/repository/ChatMessageQueryRepository.java (93%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/repository/ChatMessageRepository.java (80%) create mode 100644 src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepository.java create mode 100644 src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepositoryImpl.java rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/repository/ChatRoomRepository.java (63%) rename src/main/java/com/dnd/gongmuin/{chat => chatroom}/service/ChatMessageService.java (68%) create mode 100644 src/main/java/com/dnd/gongmuin/chatroom/service/ChatRoomService.java create mode 100644 src/test/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryControllerTest.java create mode 100644 src/test/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepositoryTest.java create mode 100644 src/test/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryServiceTest.java create mode 100644 src/test/java/com/dnd/gongmuin/common/fixture/ChatInquiryFixture.java diff --git a/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java b/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java deleted file mode 100644 index fed5fe37..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/controller/ChatRoomController.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.dnd.gongmuin.chat.controller; - -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest; -import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse; -import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse; -import com.dnd.gongmuin.chat.dto.response.ChatProposalResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomSimpleResponse; -import com.dnd.gongmuin.chat.dto.response.CreateChatRoomResponse; -import com.dnd.gongmuin.chat.dto.response.RejectChatResponse; -import com.dnd.gongmuin.chat.service.ChatRoomService; -import com.dnd.gongmuin.common.dto.PageResponse; -import com.dnd.gongmuin.member.domain.Member; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -@Tag(name = "채팅방 API") -@RestController -@RequiredArgsConstructor -public class ChatRoomController { - - private final ChatRoomService chatRoomService; - - @Operation(summary = "채팅방 메시지 조회 API", description = "채팅방 메시지를 최신순으로 페이징한다.") - @GetMapping("/api/chat-messages/{chatRoomId}") - public ResponseEntity> getChatMessages( - @PathVariable("chatRoomId") Long chatRoomId, - Pageable pageable - ) { - PageResponse response = - chatRoomService.getChatMessages(chatRoomId, pageable); - return ResponseEntity.ok(response); - } - - @Operation(summary = "채팅방 생성 API", description = "요청자가 답변자와의 채팅방을 생성한다.") - @PostMapping("/api/chat-rooms") - public ResponseEntity createChatRoom( - @Valid @RequestBody CreateChatRoomRequest request, - @AuthenticationPrincipal Member member - ) { - CreateChatRoomResponse response = chatRoomService.createChatRoom(request, member); - return ResponseEntity.ok(response); - } - - @Operation(summary = "채팅방 활성화 목록 조회 API", description = "회원의 채팅방 목록을 조회한다.") - @GetMapping("/api/chat-rooms") - public ResponseEntity> getChatRoomsByMember( - @AuthenticationPrincipal Member member, - Pageable pageable - ) { - PageResponse response - = chatRoomService.getChatRoomsByMember(member, pageable); - return ResponseEntity.ok(response); - } - - @Operation(summary = "채팅방 요청 목록 조회 API", description = "회원의 채팅방 목록을 조회한다.") - @GetMapping("/api/chat-rooms/proposals") - public ResponseEntity> getChatProposalsByMember( - @AuthenticationPrincipal Member member, - Pageable pageable - ) { - PageResponse response - = chatRoomService.getChatProposalsByMember(member, pageable); - return ResponseEntity.ok(response); - } - - @Operation(summary = "채팅방 조회 API", description = "채팅방 아이디로 채팅방을 조회한다.") - @GetMapping("/api/chat-rooms/{chatRoomId}") - public ResponseEntity createChatRoom( - @PathVariable("chatRoomId") Long chatRoomId, - @AuthenticationPrincipal Member member - ) { - ChatRoomDetailResponse response = chatRoomService.getChatRoomById(chatRoomId, member); - return ResponseEntity.ok(response); - } - - @Operation(summary = "채팅 수락 API", description = "채팅방에서 요청자와의 채팅을 수락한다.") - @PatchMapping("/api/chat-rooms/{chatRoomId}/accept") - public ResponseEntity acceptChat( - @PathVariable("chatRoomId") Long chatRoomId, - @AuthenticationPrincipal Member member - ) { - AcceptChatResponse response = chatRoomService.acceptChat(chatRoomId, member); - return ResponseEntity.ok(response); - } - - @Operation(summary = "채팅 거절 API", description = "채팅방에서 요청자와의 채팅을 거절한다.") - @PatchMapping("/api/chat-rooms/{chatRoomId}/reject") - public ResponseEntity rejectChat( - @PathVariable("chatRoomId") Long chatRoomId, - @AuthenticationPrincipal Member member - ) { - RejectChatResponse response = chatRoomService.rejectChat(chatRoomId, member); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/ChatRoomMapper.java b/src/main/java/com/dnd/gongmuin/chat/dto/ChatRoomMapper.java deleted file mode 100644 index a759af5a..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/dto/ChatRoomMapper.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.dnd.gongmuin.chat.dto; - -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse; -import com.dnd.gongmuin.chat.dto.response.ChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.ChatProposalResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo; -import com.dnd.gongmuin.chat.dto.response.ChatRoomSimpleResponse; -import com.dnd.gongmuin.chat.dto.response.CreateChatRoomResponse; -import com.dnd.gongmuin.chat.dto.response.LatestChatMessage; -import com.dnd.gongmuin.chat.dto.response.RejectChatResponse; -import com.dnd.gongmuin.member.domain.Member; -import com.dnd.gongmuin.question_post.domain.QuestionPost; -import com.dnd.gongmuin.question_post.dto.response.MemberInfo; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class ChatRoomMapper { - - public static ChatRoom toChatRoom( - QuestionPost questionPost, - Member inquirer, - Member answerer - ) { - return ChatRoom.of( - questionPost, - inquirer, - answerer - ); - } - - public static CreateChatRoomResponse toCreateChatRoomResponse( - ChatRoom chatRoom - ) { - QuestionPost questionPost = chatRoom.getQuestionPost(); - Member answerer = chatRoom.getAnswerer(); - return new CreateChatRoomResponse( - questionPost.getId(), - questionPost.getJobGroup().getLabel(), - questionPost.getTitle(), - new MemberInfo( - answerer.getId(), - answerer.getNickname(), - answerer.getJobGroup().getLabel(), - answerer.getProfileImageNo() - ), - chatRoom.getStatus().getLabel(), - chatRoom.getInquirer().getCredit() - ); - } - - public static ChatRoomDetailResponse toChatRoomDetailResponse( - ChatRoom chatRoom, - Member chatPartner - ) { - QuestionPost questionPost = chatRoom.getQuestionPost(); - boolean isInquirer = !chatPartner.equals(chatRoom.getInquirer()); - return new ChatRoomDetailResponse( - questionPost.getId(), - questionPost.getJobGroup().getLabel(), - questionPost.getTitle(), - new MemberInfo( - chatPartner.getId(), - chatPartner.getNickname(), - chatPartner.getJobGroup().getLabel(), - chatPartner.getProfileImageNo() - ), - chatRoom.getStatus().getLabel(), - isInquirer - ); - } - - public static AcceptChatResponse toAcceptChatResponse(ChatRoom chatRoom) { - return new AcceptChatResponse( - chatRoom.getStatus().getLabel(), - chatRoom.getAnswerer().getCredit() - ); - } - - public static RejectChatResponse toRejectChatResponse(ChatRoom chatRoom) { - return new RejectChatResponse( - chatRoom.getStatus().getLabel() - ); - } - - public static ChatRoomSimpleResponse toChatRoomSimpleResponse( - ChatRoomInfo chatRoomInfo, - LatestChatMessage latestChatMessage - ) { - return new ChatRoomSimpleResponse( - chatRoomInfo.chatRoomId(), - new MemberInfo( - chatRoomInfo.partnerId(), - chatRoomInfo.partnerNickname(), - chatRoomInfo.partnerJobGroup(), - chatRoomInfo.partnerProfileImageNo() - ), - latestChatMessage.content(), - latestChatMessage.type(), - latestChatMessage.createdAt().toString() - ); - } - - public static ChatProposalResponse toChatProposalResponse( - ChatProposalInfo chatProposalInfo, - LatestChatMessage latestChatMessage - ) { - return new ChatProposalResponse( - chatProposalInfo.chatRoomId(), - chatProposalInfo.chatStatus(), - chatProposalInfo.isInquirer(), - new MemberInfo( - chatProposalInfo.partnerId(), - chatProposalInfo.partnerNickname(), - chatProposalInfo.partnerJobGroup(), - chatProposalInfo.partnerProfileImageNo() - ), - latestChatMessage.content(), - latestChatMessage.type(), - latestChatMessage.createdAt().toString() - ); - } - -} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/AcceptChatResponse.java b/src/main/java/com/dnd/gongmuin/chat/dto/response/AcceptChatResponse.java deleted file mode 100644 index 5f64d562..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/AcceptChatResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.dnd.gongmuin.chat.dto.response; - -public record AcceptChatResponse( - String chatStatus, - int credit -) { -} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalInfo.java b/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalInfo.java deleted file mode 100644 index 4fbe75d4..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalInfo.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.dnd.gongmuin.chat.dto.response; - -import com.dnd.gongmuin.chat.domain.ChatStatus; -import com.dnd.gongmuin.member.domain.JobGroup; -import com.querydsl.core.annotations.QueryProjection; - -public record ChatProposalInfo( - Long chatRoomId, - String chatStatus, - boolean isInquirer, - Long partnerId, - String partnerNickname, - String partnerJobGroup, - int partnerProfileImageNo -) { - @QueryProjection - public ChatProposalInfo( - Long chatRoomId, - ChatStatus chatStatus, - boolean isInquirer, - Long partnerId, - String partnerNickname, - JobGroup partnerJobGroup, - int partnerProfileImageNo - ) { - this( - chatRoomId, - chatStatus.getLabel(), - isInquirer, - partnerId, - partnerNickname, - partnerJobGroup.getLabel(), - partnerProfileImageNo - ); - } -} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalResponse.java b/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalResponse.java deleted file mode 100644 index 7dc09842..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatProposalResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dnd.gongmuin.chat.dto.response; - -import com.dnd.gongmuin.question_post.dto.response.MemberInfo; - -public record ChatProposalResponse ( - Long chatRoomId, - String chatStatus, - boolean isInquirer, - MemberInfo chatPartner, - String latestMessage, - String messageType, - String messageCreatedAt -){} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/CreateChatRoomResponse.java b/src/main/java/com/dnd/gongmuin/chat/dto/response/CreateChatRoomResponse.java deleted file mode 100644 index 04622f6c..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/CreateChatRoomResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dnd.gongmuin.chat.dto.response; - -import com.dnd.gongmuin.question_post.dto.response.MemberInfo; - -public record CreateChatRoomResponse( - Long questionPostId, - String targetJobGroup, - String title, - MemberInfo receiverInfo, - String chatStatus, - int credit -) { -} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/RejectChatResponse.java b/src/main/java/com/dnd/gongmuin/chat/dto/response/RejectChatResponse.java deleted file mode 100644 index ae22300e..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/RejectChatResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.dnd.gongmuin.chat.dto.response; - -public record RejectChatResponse( - String chatStatus -) { -} diff --git a/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepository.java b/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepository.java deleted file mode 100644 index 8e6e3d67..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.dnd.gongmuin.chat.repository; - -import java.util.List; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; - -import com.dnd.gongmuin.chat.domain.ChatStatus; -import com.dnd.gongmuin.chat.dto.response.ChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo; -import com.dnd.gongmuin.member.domain.Member; - -public interface ChatRoomQueryRepository { - - Slice getChatRoomsByMember(Member member, Pageable pageable); - Slice getChatProposalsByMember(Member member, Pageable pageable); - - List getAutoRejectedInquirerIds(); - - void updateChatRoomStatusRejected(); -} diff --git a/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepositoryImpl.java b/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepositoryImpl.java deleted file mode 100644 index 2d1a8342..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomQueryRepositoryImpl.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.dnd.gongmuin.chat.repository; - - -import static com.dnd.gongmuin.chat.domain.QChatRoom.*; - -import java.time.LocalDateTime; -import java.util.List; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; - -import com.dnd.gongmuin.chat.domain.ChatStatus; -import com.dnd.gongmuin.chat.dto.response.ChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo; -import com.dnd.gongmuin.chat.dto.response.QChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.QChatRoomInfo; -import com.dnd.gongmuin.member.domain.Member; -import com.querydsl.core.types.dsl.CaseBuilder; -import com.querydsl.jpa.impl.JPAQueryFactory; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class ChatRoomQueryRepositoryImpl implements ChatRoomQueryRepository { - - private final JPAQueryFactory queryFactory; - - public Slice getChatRoomsByMember( - Member member, - Pageable pageable - ) { - List content = queryFactory - .select(new QChatRoomInfo( - chatRoom.id, - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.id) - .otherwise(chatRoom.inquirer.id), - - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.nickname) - .otherwise(chatRoom.inquirer.nickname), - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.jobGroup) - .otherwise(chatRoom.inquirer.jobGroup), - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.profileImageNo) - .otherwise(chatRoom.inquirer.profileImageNo) - )) - .from(chatRoom) - .where(chatRoom.inquirer.id.eq(member.getId()) - .or(chatRoom.answerer.id.eq(member.getId())) - .and(chatRoom.status.eq(ChatStatus.ACCEPTED))) - .fetch(); - - boolean hasNext = hasNext(pageable.getPageSize(), content); - return new SliceImpl<>(content, pageable, hasNext); - } - - public Slice getChatProposalsByMember(Member member, Pageable pageable){ - List content = queryFactory - .select(new QChatProposalInfo( - chatRoom.id, - chatRoom.status, - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(true) - .otherwise(false), - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.id) - .otherwise(chatRoom.inquirer.id), - - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.nickname) - .otherwise(chatRoom.inquirer.nickname), - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.jobGroup) - .otherwise(chatRoom.inquirer.jobGroup), - new CaseBuilder() - .when(chatRoom.inquirer.id.eq(member.getId())) - .then(chatRoom.answerer.profileImageNo) - .otherwise(chatRoom.inquirer.profileImageNo) - )) - .from(chatRoom) - .where(chatRoom.inquirer.id.eq(member.getId()) - .or(chatRoom.answerer.id.eq(member.getId())) - .and(chatRoom.status.in(List.of(ChatStatus.REJECTED, ChatStatus.PENDING)))) - .fetch(); - - boolean hasNext = hasNext(pageable.getPageSize(), content); - return new SliceImpl<>(content, pageable, hasNext); - } - - public List getAutoRejectedInquirerIds() { - return queryFactory - .select(chatRoom.inquirer.id) - .from(chatRoom) - .where( - chatRoom.createdAt.loe(LocalDateTime.now().minusWeeks(1)), - chatRoom.status.eq(ChatStatus.PENDING) - ) - .fetch(); - } - - public void updateChatRoomStatusRejected() { - queryFactory.update(chatRoom) - .set(chatRoom.status, ChatStatus.REJECTED) - .where( - chatRoom.createdAt.loe(LocalDateTime.now().minusWeeks(1)), - chatRoom.status.eq(ChatStatus.PENDING) - ) - .execute(); - } - - private boolean hasNext(int pageSize, List items) { - if (items.size() <= pageSize) { - return false; - } - items.remove(pageSize); - return true; - } -} diff --git a/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java b/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java deleted file mode 100644 index 8e6bd754..00000000 --- a/src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.dnd.gongmuin.chat.service; - -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.dto.ChatMessageMapper; -import com.dnd.gongmuin.chat.dto.ChatRoomMapper; -import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest; -import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse; -import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse; -import com.dnd.gongmuin.chat.dto.response.ChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.ChatProposalResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo; -import com.dnd.gongmuin.chat.dto.response.ChatRoomSimpleResponse; -import com.dnd.gongmuin.chat.dto.response.CreateChatRoomResponse; -import com.dnd.gongmuin.chat.dto.response.LatestChatMessage; -import com.dnd.gongmuin.chat.dto.response.RejectChatResponse; -import com.dnd.gongmuin.chat.exception.ChatErrorCode; -import com.dnd.gongmuin.chat.repository.ChatMessageQueryRepository; -import com.dnd.gongmuin.chat.repository.ChatMessageRepository; -import com.dnd.gongmuin.chat.repository.ChatRoomRepository; -import com.dnd.gongmuin.common.dto.PageMapper; -import com.dnd.gongmuin.common.dto.PageResponse; -import com.dnd.gongmuin.common.exception.runtime.NotFoundException; -import com.dnd.gongmuin.common.exception.runtime.ValidationException; -import com.dnd.gongmuin.credit_history.domain.CreditType; -import com.dnd.gongmuin.credit_history.service.CreditHistoryService; -import com.dnd.gongmuin.member.domain.Member; -import com.dnd.gongmuin.member.exception.MemberErrorCode; -import com.dnd.gongmuin.member.repository.MemberRepository; -import com.dnd.gongmuin.notification.domain.NotificationType; -import com.dnd.gongmuin.notification.dto.NotificationEvent; -import com.dnd.gongmuin.question_post.domain.QuestionPost; -import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; -import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class ChatRoomService { - - private static final int CHAT_REWARD = 2000; - private final ChatMessageRepository chatMessageRepository; - private final ChatMessageQueryRepository chatMessageQueryRepository; - private final ChatRoomRepository chatRoomRepository; - private final MemberRepository memberRepository; - private final QuestionPostRepository questionPostRepository; - private final CreditHistoryService creditHistoryService; - private final ApplicationEventPublisher eventPublisher; - - private static void validateIfAnswerer(Member member, ChatRoom chatRoom) { - if (!Objects.equals(member.getId(), chatRoom.getAnswerer().getId())) { - throw new ValidationException(ChatErrorCode.UNAUTHORIZED_REQUEST); - } - } - - @Transactional(readOnly = true) - public PageResponse getChatMessages(Long chatRoomId, Pageable pageable) { - Slice responsePage = chatMessageRepository - .findByChatRoomIdOrderByCreatedAtDesc(chatRoomId, pageable) - .map(ChatMessageMapper::toChatMessageResponse); - return PageMapper.toPageResponse(responsePage); - } - - @Transactional - public CreateChatRoomResponse createChatRoom(CreateChatRoomRequest request, Member inquirer) { - QuestionPost questionPost = getQuestionPostById(request.questionPostId()); - Member answerer = getMemberById(request.answererId()); - - ChatRoom chatRoom = chatRoomRepository.save( - ChatRoomMapper.toChatRoom(questionPost, inquirer, answerer) - ); - chatMessageRepository.save( - ChatMessageMapper.toFirstChatMessage(chatRoom) - ); - creditHistoryService.saveChatCreditHistory(CreditType.CHAT_REQUEST, inquirer); - - eventPublisher.publishEvent( - new NotificationEvent(NotificationType.CHAT_REQUEST, chatRoom.getId(), inquirer.getId(), answerer) - ); - - return ChatRoomMapper.toCreateChatRoomResponse(chatRoom); - } - - @Transactional(readOnly = true) - public PageResponse getChatRoomsByMember(Member member, Pageable pageable) { - // 회원 채팅방 정보 가져오기 - Slice chatRoomInfos = chatRoomRepository.getChatRoomsByMember( - member, pageable - ); - - // chatRoomId 리스트 추출 - List chatRoomIds = chatRoomInfos.stream() - .map(ChatRoomInfo::chatRoomId) - .toList(); - - // 각 채팅방 최근 메시지 가져오기 - List latestChatMessages - = chatMessageQueryRepository.findLatestChatByChatRoomIds(chatRoomIds); - - // 두 객체 합쳐서 하나의 DTO로 반환 - List responses = getChatRoomSimpleResponses(latestChatMessages, - chatRoomInfos); - - // PageResponse 객체 생성 - return new PageResponse<>(responses, responses.size(), chatRoomInfos.hasNext()); - } - - @Transactional(readOnly = true) - public PageResponse getChatProposalsByMember(Member member, Pageable pageable) { - Slice chatProposalInfos = chatRoomRepository.getChatProposalsByMember( - member, pageable - ); - - List chatRoomIds = chatProposalInfos.stream() - .map(ChatProposalInfo::chatRoomId) - .toList(); - - List latestChatMessages - = chatMessageQueryRepository.findLatestChatByChatRoomIds(chatRoomIds); - - List responses = getChatProposalResponse(latestChatMessages, - chatProposalInfos); - - return new PageResponse<>(responses, responses.size(), chatProposalInfos.hasNext()); - } - - - @Transactional(readOnly = true) - public ChatRoomDetailResponse getChatRoomById(Long chatRoomId, Member member) { - ChatRoom chatRoom = getChatRoomById(chatRoomId); - Member chatPartner = getChatPartner(member.getId(), chatRoom); - return ChatRoomMapper.toChatRoomDetailResponse(chatRoom, chatPartner); - } - - @Transactional - public AcceptChatResponse acceptChat(Long chatRoomId, Member answerer) { - ChatRoom chatRoom = getChatRoomById(chatRoomId); - validateIfAnswerer(answerer, chatRoom); - chatRoom.updateStatusAccepted(); - creditHistoryService.saveChatCreditHistory(CreditType.CHAT_ACCEPT, answerer); - eventPublisher.publishEvent( - new NotificationEvent(NotificationType.CHAT_ACCEPT, chatRoom.getId(), answerer.getId(), - chatRoom.getInquirer()) - ); - - return ChatRoomMapper.toAcceptChatResponse(chatRoom); - } - - @Transactional - public RejectChatResponse rejectChat(Long chatRoomId, Member answerer) { - ChatRoom chatRoom = getChatRoomById(chatRoomId); - validateIfAnswerer(answerer, chatRoom); - chatRoom.updateStatusRejected(); - creditHistoryService.saveChatCreditHistory(CreditType.CHAT_REFUND, chatRoom.getInquirer()); - eventPublisher.publishEvent( - new NotificationEvent(NotificationType.CHAT_REJECT, chatRoom.getId(), answerer.getId(), - chatRoom.getInquirer()) - ); - - return ChatRoomMapper.toRejectChatResponse(chatRoom); - } - - @Transactional - public void rejectChatAuto() { - List rejectedInquirerIds = chatRoomRepository.getAutoRejectedInquirerIds(); - chatRoomRepository.updateChatRoomStatusRejected(); - memberRepository.refundInMemberIds(rejectedInquirerIds, CHAT_REWARD); - creditHistoryService.saveCreditHistoryInMemberIds(rejectedInquirerIds, CreditType.CHAT_REFUND, CHAT_REWARD); - } - - private List getChatRoomSimpleResponses(List latestChatMessages, - Slice chatRoomInfos) { - // -> 순서 보장 x - Map messageMap = latestChatMessages.stream() - .collect(Collectors.toMap(LatestChatMessage::chatRoomId, message -> message)); - - // 최신순 정렬 및 변환 - return chatRoomInfos.stream() - .sorted( - Comparator.comparing( - (ChatRoomInfo info) -> messageMap.get(info.chatRoomId()).createdAt() - ).reversed()) - .map(chatRoomInfo -> { - LatestChatMessage latestMessage = messageMap.get(chatRoomInfo.chatRoomId()); - return ChatRoomMapper.toChatRoomSimpleResponse( - chatRoomInfo, latestMessage - ); - }).toList(); - } - - private List getChatProposalResponse(List latestChatMessages, - Slice chatProposalInfos) { - // -> 순서 보장 x - Map messageMap = latestChatMessages.stream() - .collect(Collectors.toMap(LatestChatMessage::chatRoomId, message -> message)); - - // 최신순 정렬 및 변환 - return chatProposalInfos.stream() - .sorted( - Comparator.comparing( - (ChatProposalInfo info) -> messageMap.get(info.chatRoomId()).createdAt() - ).reversed()) - .map(chatProposalInfo -> { - LatestChatMessage latestMessage = messageMap.get(chatProposalInfo.chatRoomId()); - return ChatRoomMapper.toChatProposalResponse( - chatProposalInfo, latestMessage - ); - }).toList(); - } - - private ChatRoom getChatRoomById(Long id) { - return chatRoomRepository.findById(id) - .orElseThrow(() -> new NotFoundException(ChatErrorCode.NOT_FOUND_CHAT_ROOM)); - } - - private QuestionPost getQuestionPostById(Long id) { - return questionPostRepository.findById(id) - .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); - } - - private Member getMemberById(Long id) { - return memberRepository.findById(id) - .orElseThrow(() -> new NotFoundException(MemberErrorCode.NOT_FOUND_MEMBER)); - } - - private Member getChatPartner(Long memberId, ChatRoom chatRoom) { - if (Objects.equals(chatRoom.getAnswerer().getId(), memberId)) { - return chatRoom.getInquirer(); - } else if (Objects.equals(chatRoom.getInquirer().getId(), memberId)) { - return chatRoom.getAnswerer(); - } else { - throw new ValidationException(ChatErrorCode.UNAUTHORIZED_CHAT_ROOM); - } - } -} - diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryController.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryController.java new file mode 100644 index 00000000..4ee2e0e4 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryController.java @@ -0,0 +1,75 @@ +package com.dnd.gongmuin.chat_inquiry.controller; + +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.dnd.gongmuin.chat_inquiry.dto.AcceptChatResponse; +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryRequest; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.RejectChatResponse; +import com.dnd.gongmuin.chat_inquiry.service.ChatInquiryService; +import com.dnd.gongmuin.common.dto.PageResponse; +import com.dnd.gongmuin.member.domain.Member; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@Tag(name = "채팅 요청 API") +@RestController +@RequiredArgsConstructor +public class ChatInquiryController { + + private final ChatInquiryService chatInquiryService; + + @Operation(summary = "채팅 요청 생성 API", description = "답변자 아이디로 채팅 요청을 생성한다.") + @PostMapping("/api/chat/inquiries") + public ResponseEntity createChatInquiry( + @Valid @RequestBody CreateChatInquiryRequest request, + @AuthenticationPrincipal Member member + ) { + CreateChatInquiryResponse response + = chatInquiryService.createChatInquiry(request, member); + return ResponseEntity.ok(response); + } + + @Operation(summary = "채팅방 요청 목록 조회 API", description = "회원의 채팅방 목록을 조회한다.") + @GetMapping("/api/chat/inquiries") + public ResponseEntity> getChatInquiresByMember( + @AuthenticationPrincipal Member member, + Pageable pageable + ) { + PageResponse response + = chatInquiryService.getChatInquiresByMember(member, pageable); + return ResponseEntity.ok(response); + } + + @Operation(summary = "채팅 수락 API", description = "채팅방에서 요청자와의 채팅을 수락한다.") + @PatchMapping("/api/chat/inquiries/{chatInquiryId}/accept") + public ResponseEntity acceptChat( + @PathVariable("chatInquiryId") Long chatInquiryId, + @AuthenticationPrincipal Member member + ) { + AcceptChatResponse response = chatInquiryService.acceptChat(chatInquiryId, member); + return ResponseEntity.ok(response); + } + + @Operation(summary = "채팅 거절 API", description = "채팅방에서 요청자와의 채팅을 거절한다.") + @PatchMapping("/api/chat/inquiries/{chatInquiryId}/reject") + public ResponseEntity rejectChat( + @PathVariable("chatInquiryId") Long chatInquiryId, + @AuthenticationPrincipal Member member + ) { + RejectChatResponse response = chatInquiryService.rejectChat(chatInquiryId, member); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/domain/ChatRoom.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/domain/ChatInquiry.java similarity index 72% rename from src/main/java/com/dnd/gongmuin/chat/domain/ChatRoom.java rename to src/main/java/com/dnd/gongmuin/chat_inquiry/domain/ChatInquiry.java index 6ee3898d..86efde25 100644 --- a/src/main/java/com/dnd/gongmuin/chat/domain/ChatRoom.java +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/domain/ChatInquiry.java @@ -1,10 +1,10 @@ -package com.dnd.gongmuin.chat.domain; +package com.dnd.gongmuin.chat_inquiry.domain; import static jakarta.persistence.ConstraintMode.*; import static jakarta.persistence.EnumType.*; import static jakarta.persistence.FetchType.*; -import com.dnd.gongmuin.chat.exception.ChatErrorCode; +import com.dnd.gongmuin.chatroom.exception.ChatErrorCode; import com.dnd.gongmuin.common.entity.TimeBaseEntity; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.member.domain.Member; @@ -26,13 +26,12 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ChatRoom extends TimeBaseEntity { - +public class ChatInquiry extends TimeBaseEntity { private static final int CHAT_REWARD = 2000; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "chat_room_id", nullable = false) + @Column(name = "chat_proposal_id", nullable = false) private Long id; @ManyToOne(fetch = LAZY) @@ -53,37 +52,42 @@ public class ChatRoom extends TimeBaseEntity { @Enumerated(STRING) @Column(name = "status", nullable = false) - private ChatStatus status; + private InquiryStatus status; + + @Column(name = "message", nullable = false) + private String message; - private ChatRoom(QuestionPost questionPost, Member inquirer, Member answerer) { + private ChatInquiry(QuestionPost questionPost, Member inquirer, Member answerer, String message) { this.questionPost = questionPost; this.inquirer = inquirer; this.answerer = answerer; - this.status = ChatStatus.PENDING; + this.status = InquiryStatus.PENDING; + this.message = message; inquirer.decreaseCredit(CHAT_REWARD); } - public static ChatRoom of( + public static ChatInquiry of( QuestionPost questionPost, Member inquirer, - Member answerer + Member answerer, + String message ) { - return new ChatRoom(questionPost, inquirer, answerer); + return new ChatInquiry(questionPost, inquirer, answerer, message); } public void updateStatusAccepted() { - if (status != ChatStatus.PENDING) { + if (status != InquiryStatus.PENDING) { throw new ValidationException(ChatErrorCode.UNABLE_TO_CHANGE_CHAT_STATUS); } - status = ChatStatus.ACCEPTED; + status = InquiryStatus.ACCEPTED; answerer.increaseCredit(CHAT_REWARD); } public void updateStatusRejected() { - if (status != ChatStatus.PENDING) { + if (status != InquiryStatus.PENDING) { throw new ValidationException(ChatErrorCode.UNABLE_TO_CHANGE_CHAT_STATUS); } - status = ChatStatus.REJECTED; + status = InquiryStatus.REJECTED; inquirer.increaseCredit(CHAT_REWARD); } } diff --git a/src/main/java/com/dnd/gongmuin/chat/domain/ChatStatus.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/domain/InquiryStatus.java similarity index 75% rename from src/main/java/com/dnd/gongmuin/chat/domain/ChatStatus.java rename to src/main/java/com/dnd/gongmuin/chat_inquiry/domain/InquiryStatus.java index beef2fbe..4a940ad0 100644 --- a/src/main/java/com/dnd/gongmuin/chat/domain/ChatStatus.java +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/domain/InquiryStatus.java @@ -1,8 +1,8 @@ -package com.dnd.gongmuin.chat.domain; +package com.dnd.gongmuin.chat_inquiry.domain; import java.util.Arrays; -import com.dnd.gongmuin.chat.exception.ChatErrorCode; +import com.dnd.gongmuin.chatroom.exception.ChatErrorCode; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import lombok.Getter; @@ -10,7 +10,7 @@ @Getter @RequiredArgsConstructor -public enum ChatStatus { +public enum InquiryStatus { PENDING("요청중"), ACCEPTED("수락됨"), @@ -18,7 +18,7 @@ public enum ChatStatus { private final String label; - public static ChatStatus from(String input) { + public static InquiryStatus from(String input) { return Arrays.stream(values()) .filter(status -> status.isEqual(input)) .findAny() diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/AcceptChatResponse.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/AcceptChatResponse.java new file mode 100644 index 00000000..64fa6e01 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/AcceptChatResponse.java @@ -0,0 +1,8 @@ +package com.dnd.gongmuin.chat_inquiry.dto; + +public record AcceptChatResponse( + Long createdChatRoomId, + String inquiryStatus, + int credit +) { +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryMapper.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryMapper.java new file mode 100644 index 00000000..4a3486ec --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryMapper.java @@ -0,0 +1,63 @@ +package com.dnd.gongmuin.chat_inquiry.dto; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; +import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.dto.response.MemberInfo; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ChatInquiryMapper { + + public static ChatInquiry toChatInquiry( + QuestionPost questionPost, + Member inquirer, + Member answerer, + String message + ) { + return ChatInquiry.of( + questionPost, + inquirer, + answerer, + message + ); + } + + public static CreateChatInquiryResponse toCreateChatInquiryResponse( + ChatInquiry chatInquiry + ) { + Member answerer = chatInquiry.getAnswerer(); + return new CreateChatInquiryResponse( + chatInquiry.getId(), + chatInquiry.getMessage(), + InquiryStatus.PENDING.getLabel(), + new MemberInfo( + answerer.getId(), + answerer.getNickname(), + answerer.getJobGroup().getLabel(), + answerer.getProfileImageNo() + ) + ); + } + + public static AcceptChatResponse toAcceptChatResponse( + ChatInquiry chatInquiry, + ChatRoom chatRoom + ) { + return new AcceptChatResponse( + chatRoom.getId(), + chatInquiry.getStatus().getLabel(), + chatInquiry.getAnswerer().getCredit() + ); + } + + public static RejectChatResponse toRejectChatResponse(ChatInquiry chatInquiry) { + return new RejectChatResponse( + chatInquiry.getStatus().getLabel() + ); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java new file mode 100644 index 00000000..6eaf7fdc --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java @@ -0,0 +1,39 @@ +package com.dnd.gongmuin.chat_inquiry.dto; + +import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; +import com.dnd.gongmuin.member.domain.JobGroup; +import com.dnd.gongmuin.question_post.dto.response.MemberInfo; +import com.querydsl.core.annotations.QueryProjection; + +public record ChatInquiryResponse( + Long chatInquiryId, + String message, + String inquiryStatus, + boolean isInquirer, + MemberInfo partnerInfo +) { + @QueryProjection + public ChatInquiryResponse( + Long chatInquiryId, + String inquiryMessage, + InquiryStatus inquiryStatus, + boolean isInquirer, + Long partnerId, + String partnerNickname, + JobGroup partnerJobGroup, + int partnerProfileImageNo + ) { + this( + chatInquiryId, + inquiryMessage, + inquiryStatus.getLabel(), + isInquirer, + new MemberInfo( + partnerId, + partnerNickname, + partnerJobGroup.getLabel(), + partnerProfileImageNo + ) + ); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryRequest.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryRequest.java new file mode 100644 index 00000000..c88d4c2a --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryRequest.java @@ -0,0 +1,15 @@ +package com.dnd.gongmuin.chat_inquiry.dto; + +import jakarta.validation.constraints.NotNull; + +public record CreateChatInquiryRequest( + @NotNull(message = "질문 게시글 아이디는 필수 입력 항목입니다.") + Long questionPostId, + + @NotNull(message = "답변자 아이디는 필수 입력 항목입니다.") + Long answererId, + + @NotNull(message = "요청 메시지는 필수 입력 항목입니다.") + String message +) { +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryResponse.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryResponse.java new file mode 100644 index 00000000..6986b83b --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/CreateChatInquiryResponse.java @@ -0,0 +1,11 @@ +package com.dnd.gongmuin.chat_inquiry.dto; + +import com.dnd.gongmuin.question_post.dto.response.MemberInfo; + +public record CreateChatInquiryResponse( + Long chatInquiryId, + String inquiryMessage, + String inquiryStatus, + MemberInfo partnerInfo +) { +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/RejectChatResponse.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/RejectChatResponse.java new file mode 100644 index 00000000..750fb01c --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/RejectChatResponse.java @@ -0,0 +1,6 @@ +package com.dnd.gongmuin.chat_inquiry.dto; + +public record RejectChatResponse( + String inquiryStatus +) { +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepository.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepository.java new file mode 100644 index 00000000..126d693f --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepository.java @@ -0,0 +1,17 @@ +package com.dnd.gongmuin.chat_inquiry.repository; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryResponse; +import com.dnd.gongmuin.member.domain.Member; + +public interface ChatInquiryQueryRepository { + Slice getChatInquiresByMember(Member member, Pageable pageable); + + List getAutoRejectedInquirerIds(); + + void updateChatInquiryStatusRejected(); +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepositoryImpl.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepositoryImpl.java new file mode 100644 index 00000000..0510d2df --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryQueryRepositoryImpl.java @@ -0,0 +1,92 @@ +package com.dnd.gongmuin.chat_inquiry.repository; + +import static com.dnd.gongmuin.chat_inquiry.domain.QChatInquiry.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.QChatInquiryResponse; +import com.dnd.gongmuin.member.domain.Member; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ChatInquiryQueryRepositoryImpl implements ChatInquiryQueryRepository { + + private final JPAQueryFactory queryFactory; + + public Slice getChatInquiresByMember(Member member, Pageable pageable) { + List content = queryFactory + .select(new QChatInquiryResponse( + chatInquiry.id, + chatInquiry.message, + chatInquiry.status, + new CaseBuilder() + .when(chatInquiry.inquirer.id.eq(member.getId())) + .then(true) + .otherwise(false), + new CaseBuilder() + .when(chatInquiry.inquirer.id.eq(member.getId())) + .then(chatInquiry.answerer.id) + .otherwise(chatInquiry.inquirer.id), + new CaseBuilder() + .when(chatInquiry.inquirer.id.eq(member.getId())) + .then(chatInquiry.answerer.nickname) + .otherwise(chatInquiry.inquirer.nickname), + new CaseBuilder() + .when(chatInquiry.inquirer.id.eq(member.getId())) + .then(chatInquiry.answerer.jobGroup) + .otherwise(chatInquiry.inquirer.jobGroup), + new CaseBuilder() + .when(chatInquiry.inquirer.id.eq(member.getId())) + .then(chatInquiry.answerer.profileImageNo) + .otherwise(chatInquiry.inquirer.profileImageNo) + )) + .from(chatInquiry) + .where(chatInquiry.inquirer.id.eq(member.getId()) + .or(chatInquiry.answerer.id.eq(member.getId())) + .and(chatInquiry.status.in(List.of(InquiryStatus.REJECTED, InquiryStatus.PENDING)))) + .orderBy(chatInquiry.createdAt.desc()) + .fetch(); + + boolean hasNext = hasNext(pageable.getPageSize(), content); + return new SliceImpl<>(content, pageable, hasNext); + } + + public List getAutoRejectedInquirerIds() { + return queryFactory + .select(chatInquiry.inquirer.id) + .from(chatInquiry) + .where( + chatInquiry.createdAt.loe(LocalDateTime.now().minusWeeks(1)), + chatInquiry.status.eq(InquiryStatus.PENDING) + ) + .fetch(); + } + + public void updateChatInquiryStatusRejected() { + queryFactory.update(chatInquiry) + .set(chatInquiry.status, InquiryStatus.REJECTED) + .where( + chatInquiry.createdAt.loe(LocalDateTime.now().minusWeeks(1)), + chatInquiry.status.eq(InquiryStatus.PENDING) + ) + .execute(); + } + + private boolean hasNext(int pageSize, List items) { + if (items.size() <= pageSize) { + return false; + } + items.remove(pageSize); + return true; + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepository.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepository.java new file mode 100644 index 00000000..a10cb885 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepository.java @@ -0,0 +1,8 @@ +package com.dnd.gongmuin.chat_inquiry.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; + +public interface ChatInquiryRepository extends JpaRepository, ChatInquiryQueryRepository { +} diff --git a/src/main/java/com/dnd/gongmuin/chat/scheduler/ChatScheduler.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/scheduler/ChatInquiryScheduler.java similarity index 54% rename from src/main/java/com/dnd/gongmuin/chat/scheduler/ChatScheduler.java rename to src/main/java/com/dnd/gongmuin/chat_inquiry/scheduler/ChatInquiryScheduler.java index 54a0e03e..c1eb3213 100644 --- a/src/main/java/com/dnd/gongmuin/chat/scheduler/ChatScheduler.java +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/scheduler/ChatInquiryScheduler.java @@ -1,23 +1,22 @@ -package com.dnd.gongmuin.chat.scheduler; +package com.dnd.gongmuin.chat_inquiry.scheduler; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import com.dnd.gongmuin.chat.service.ChatRoomService; +import com.dnd.gongmuin.chat_inquiry.service.ChatInquiryService; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor -public class ChatScheduler { +public class ChatInquiryScheduler { - private final ChatRoomService chatRoomService; + private final ChatInquiryService chatInquiryService; @Transactional @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") - public void rejectChatRequest() { - chatRoomService.rejectChatAuto(); + public void rejectChatInquiry() { + chatInquiryService.rejectChatAuto(); } - } diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryService.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryService.java new file mode 100644 index 00000000..4d8ea017 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryService.java @@ -0,0 +1,139 @@ +package com.dnd.gongmuin.chat_inquiry.service; + +import java.util.List; +import java.util.Objects; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; +import com.dnd.gongmuin.chat_inquiry.dto.AcceptChatResponse; +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryMapper; +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryRequest; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.RejectChatResponse; +import com.dnd.gongmuin.chat_inquiry.repository.ChatInquiryRepository; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.dto.ChatRoomMapper; +import com.dnd.gongmuin.chatroom.exception.ChatErrorCode; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; +import com.dnd.gongmuin.common.dto.PageMapper; +import com.dnd.gongmuin.common.dto.PageResponse; +import com.dnd.gongmuin.common.exception.runtime.NotFoundException; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; +import com.dnd.gongmuin.credit_history.domain.CreditType; +import com.dnd.gongmuin.credit_history.service.CreditHistoryService; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.exception.MemberErrorCode; +import com.dnd.gongmuin.member.repository.MemberRepository; +import com.dnd.gongmuin.notification.domain.NotificationType; +import com.dnd.gongmuin.notification.dto.NotificationEvent; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatInquiryService { + + private static final int CHAT_REWARD = 2000; + private final ChatInquiryRepository chatInquiryRepository; + private final ChatRoomRepository chatRoomRepository; + private final MemberRepository memberRepository; + private final QuestionPostRepository questionPostRepository; + private final CreditHistoryService creditHistoryService; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public CreateChatInquiryResponse createChatInquiry(CreateChatInquiryRequest request, Member inquirer) { + QuestionPost questionPost = getQuestionPostById(request.questionPostId()); + Member answerer = getMemberById(request.answererId()); + + ChatInquiry chatInquiry = chatInquiryRepository.save( + ChatInquiryMapper.toChatInquiry(questionPost, inquirer, answerer, request.message()) + ); + + eventPublisher.publishEvent( + new NotificationEvent(NotificationType.CHAT_REQUEST, chatInquiry.getId(), inquirer.getId(), answerer) + ); + creditHistoryService.saveChatCreditHistory(CreditType.CHAT_REQUEST, inquirer); + + return ChatInquiryMapper.toCreateChatInquiryResponse(chatInquiry); + } + + @Transactional(readOnly = true) + public PageResponse getChatInquiresByMember(Member member, Pageable pageable) { + Slice responsePage = chatInquiryRepository.getChatInquiresByMember( + member, pageable + ); + return PageMapper.toPageResponse(responsePage); + } + + @Transactional + public AcceptChatResponse acceptChat(Long chatInquiryId, Member answerer) { + ChatInquiry chatInquiry = getChatProposalById(chatInquiryId); + validateIfAnswerer(answerer, chatInquiry); + chatInquiry.updateStatusAccepted(); + creditHistoryService.saveChatCreditHistory(CreditType.CHAT_ACCEPT, answerer); + + ChatRoom chatRoom = chatRoomRepository.save( + ChatRoomMapper.toChatRoom(chatInquiry.getQuestionPost(), chatInquiry.getInquirer(), answerer) + ); + eventPublisher.publishEvent( + new NotificationEvent(NotificationType.CHAT_ACCEPT, chatInquiry.getId(), answerer.getId(), + chatInquiry.getInquirer()) + ); + + return ChatInquiryMapper.toAcceptChatResponse(chatInquiry, chatRoom); + } + + @Transactional + public RejectChatResponse rejectChat(Long chatInquiryId, Member answerer) { + ChatInquiry chatInquiry = getChatProposalById(chatInquiryId); + + validateIfAnswerer(answerer, chatInquiry); + chatInquiry.updateStatusRejected(); + creditHistoryService.saveChatCreditHistory(CreditType.CHAT_REFUND, chatInquiry.getInquirer()); + eventPublisher.publishEvent( + new NotificationEvent(NotificationType.CHAT_REJECT, chatInquiry.getId(), answerer.getId(), + chatInquiry.getInquirer()) + ); + + return ChatInquiryMapper.toRejectChatResponse(chatInquiry); + } + + @Transactional + public void rejectChatAuto() { + List rejectedInquirerIds = chatInquiryRepository.getAutoRejectedInquirerIds(); + chatInquiryRepository.updateChatInquiryStatusRejected(); + memberRepository.refundInMemberIds(rejectedInquirerIds, CHAT_REWARD); + creditHistoryService.saveCreditHistoryInMemberIds(rejectedInquirerIds, CreditType.CHAT_REFUND, CHAT_REWARD); + } + + private ChatInquiry getChatProposalById(Long id) { + return chatInquiryRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ChatErrorCode.NOT_FOUND_CHAT_ROOM)); + } + + private static void validateIfAnswerer(Member member, ChatInquiry chatInquiry) { + if (!Objects.equals(member.getId(), chatInquiry.getAnswerer().getId())) { + throw new ValidationException(ChatErrorCode.UNAUTHORIZED_REQUEST); + } + } + + private QuestionPost getQuestionPostById(Long id) { + return questionPostRepository.findById(id) + .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); + } + + private Member getMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.NOT_FOUND_MEMBER)); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/controller/ChatMessageController.java b/src/main/java/com/dnd/gongmuin/chatroom/controller/ChatMessageController.java similarity index 76% rename from src/main/java/com/dnd/gongmuin/chat/controller/ChatMessageController.java rename to src/main/java/com/dnd/gongmuin/chatroom/controller/ChatMessageController.java index 0ce6aac0..f35016cd 100644 --- a/src/main/java/com/dnd/gongmuin/chat/controller/ChatMessageController.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/controller/ChatMessageController.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.controller; +package com.dnd.gongmuin.chatroom.controller; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -6,9 +6,9 @@ import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; -import com.dnd.gongmuin.chat.dto.request.ChatMessageRequest; -import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse; -import com.dnd.gongmuin.chat.service.ChatMessageService; +import com.dnd.gongmuin.chatroom.dto.request.ChatMessageRequest; +import com.dnd.gongmuin.chatroom.dto.response.ChatMessageResponse; +import com.dnd.gongmuin.chatroom.service.ChatMessageService; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/dnd/gongmuin/chatroom/controller/ChatRoomController.java b/src/main/java/com/dnd/gongmuin/chatroom/controller/ChatRoomController.java new file mode 100644 index 00000000..79ab0a10 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chatroom/controller/ChatRoomController.java @@ -0,0 +1,60 @@ +package com.dnd.gongmuin.chatroom.controller; + +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import com.dnd.gongmuin.chatroom.dto.response.ChatMessageResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomDetailResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomSimpleResponse; +import com.dnd.gongmuin.chatroom.service.ChatRoomService; +import com.dnd.gongmuin.common.dto.PageResponse; +import com.dnd.gongmuin.member.domain.Member; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "채팅방 API") +@RestController +@RequiredArgsConstructor +public class ChatRoomController { + + private final ChatRoomService chatRoomService; + + @Operation(summary = "채팅방 메시지 조회 API", description = "채팅방 메시지를 최신순으로 페이징한다.") + @GetMapping("/api/chat-messages/{chatRoomId}") + public ResponseEntity> getChatMessages( + @PathVariable("chatRoomId") Long chatRoomId, + Pageable pageable + ) { + PageResponse response = + chatRoomService.getChatMessages(chatRoomId, pageable); + return ResponseEntity.ok(response); + } + + @Operation(summary = "채팅방 활성화 목록 조회 API", description = "회원의 채팅방 목록을 조회한다.") + @GetMapping("/api/chat-rooms") + public ResponseEntity> getChatRoomsByMember( + @AuthenticationPrincipal Member member, + Pageable pageable + ) { + PageResponse response + = chatRoomService.getChatRoomsByMember(member, pageable); + return ResponseEntity.ok(response); + } + + @Operation(summary = "채팅방 조회 API", description = "채팅방 아이디로 채팅방을 조회한다.") + @GetMapping("/api/chat-rooms/{chatRoomId}") + public ResponseEntity createChatRoom( + @PathVariable("chatRoomId") Long chatRoomId, + @AuthenticationPrincipal Member member + ) { + ChatRoomDetailResponse response = chatRoomService.getChatRoomById(chatRoomId, member); + return ResponseEntity.ok(response); + } + +} \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java b/src/main/java/com/dnd/gongmuin/chatroom/domain/ChatMessage.java similarity index 95% rename from src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java rename to src/main/java/com/dnd/gongmuin/chatroom/domain/ChatMessage.java index b41bf5ab..ce72029e 100644 --- a/src/main/java/com/dnd/gongmuin/chat/domain/ChatMessage.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/domain/ChatMessage.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.domain; +package com.dnd.gongmuin.chatroom.domain; import static lombok.AccessLevel.*; diff --git a/src/main/java/com/dnd/gongmuin/chatroom/domain/ChatRoom.java b/src/main/java/com/dnd/gongmuin/chatroom/domain/ChatRoom.java new file mode 100644 index 00000000..97a6be8c --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chatroom/domain/ChatRoom.java @@ -0,0 +1,61 @@ +package com.dnd.gongmuin.chatroom.domain; + +import static jakarta.persistence.ConstraintMode.*; +import static jakarta.persistence.FetchType.*; + +import com.dnd.gongmuin.common.entity.TimeBaseEntity; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.question_post.domain.QuestionPost; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChatRoom extends TimeBaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "chat_room_id", nullable = false) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "question_post_id", + nullable = false, + foreignKey = @ForeignKey(NO_CONSTRAINT)) + private QuestionPost questionPost; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "inquirer_id", nullable = false, + foreignKey = @ForeignKey(NO_CONSTRAINT)) + private Member inquirer; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "answerer_id", nullable = false, + foreignKey = @ForeignKey(NO_CONSTRAINT)) + private Member answerer; + + private ChatRoom(QuestionPost questionPost, Member inquirer, Member answerer) { + this.questionPost = questionPost; + this.inquirer = inquirer; + this.answerer = answerer; + } + + public static ChatRoom of( + QuestionPost questionPost, + Member inquirer, + Member answerer + ) { + return new ChatRoom(questionPost, inquirer, answerer); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/domain/MessageType.java b/src/main/java/com/dnd/gongmuin/chatroom/domain/MessageType.java similarity index 86% rename from src/main/java/com/dnd/gongmuin/chat/domain/MessageType.java rename to src/main/java/com/dnd/gongmuin/chatroom/domain/MessageType.java index 9850fee1..f469ceb1 100644 --- a/src/main/java/com/dnd/gongmuin/chat/domain/MessageType.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/domain/MessageType.java @@ -1,8 +1,8 @@ -package com.dnd.gongmuin.chat.domain; +package com.dnd.gongmuin.chatroom.domain; import java.util.Arrays; -import com.dnd.gongmuin.chat.exception.ChatErrorCode; +import com.dnd.gongmuin.chatroom.exception.ChatErrorCode; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import lombok.Getter; diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageMapper.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/ChatMessageMapper.java similarity index 76% rename from src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageMapper.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/ChatMessageMapper.java index 8771d82a..98a45f07 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/ChatMessageMapper.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/ChatMessageMapper.java @@ -1,10 +1,10 @@ -package com.dnd.gongmuin.chat.dto; +package com.dnd.gongmuin.chatroom.dto; -import com.dnd.gongmuin.chat.domain.ChatMessage; -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.domain.MessageType; -import com.dnd.gongmuin.chat.dto.request.ChatMessageRequest; -import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse; +import com.dnd.gongmuin.chatroom.domain.ChatMessage; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.domain.MessageType; +import com.dnd.gongmuin.chatroom.dto.request.ChatMessageRequest; +import com.dnd.gongmuin.chatroom.dto.response.ChatMessageResponse; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/dnd/gongmuin/chatroom/dto/ChatRoomMapper.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/ChatRoomMapper.java new file mode 100644 index 00000000..7f7eb957 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/ChatRoomMapper.java @@ -0,0 +1,77 @@ +package com.dnd.gongmuin.chatroom.dto; + +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomDetailResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomSimpleResponse; +import com.dnd.gongmuin.chatroom.dto.response.LatestChatMessage; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.dto.response.MemberInfo; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ChatRoomMapper { + + public static ChatRoom toChatRoom( + QuestionPost questionPost, + Member inquirer, + Member answerer + ) { + return ChatRoom.of( + questionPost, + inquirer, + answerer + ); + } + + public static ChatRoomDetailResponse toChatRoomDetailResponse( + ChatRoom chatRoom, + Member chatPartner + ) { + QuestionPost questionPost = chatRoom.getQuestionPost(); + boolean isInquirer = !chatPartner.equals(chatRoom.getInquirer()); + return new ChatRoomDetailResponse( + questionPost.getId(), + questionPost.getJobGroup().getLabel(), + questionPost.getTitle(), + new MemberInfo( + chatPartner.getId(), + chatPartner.getNickname(), + chatPartner.getJobGroup().getLabel(), + chatPartner.getProfileImageNo() + ), + isInquirer + ); + } + + public static ChatRoomSimpleResponse toChatRoomSimpleResponse( + ChatRoomInfo chatRoomInfo, + LatestChatMessage latestChatMessage + ) { + String content = null; + String type = null; + String createdAt = null; + + if (latestChatMessage != null) { + content = latestChatMessage.content(); + type = latestChatMessage.type(); + createdAt = latestChatMessage.createdAt().toString(); + } + + return new ChatRoomSimpleResponse( + chatRoomInfo.chatRoomId(), + new MemberInfo( + chatRoomInfo.partnerId(), + chatRoomInfo.partnerNickname(), + chatRoomInfo.partnerJobGroup(), + chatRoomInfo.partnerProfileImageNo() + ), + content, + type, + createdAt + ); + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/request/ChatMessageRequest.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/request/ChatMessageRequest.java similarity index 88% rename from src/main/java/com/dnd/gongmuin/chat/dto/request/ChatMessageRequest.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/request/ChatMessageRequest.java index 3fe88baf..650ac0c1 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/request/ChatMessageRequest.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/request/ChatMessageRequest.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.request; +package com.dnd.gongmuin.chatroom.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/request/CreateChatRoomRequest.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/request/CreateChatRoomRequest.java similarity index 86% rename from src/main/java/com/dnd/gongmuin/chat/dto/request/CreateChatRoomRequest.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/request/CreateChatRoomRequest.java index d6ae8686..de940f12 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/request/CreateChatRoomRequest.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/request/CreateChatRoomRequest.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.request; +package com.dnd.gongmuin.chatroom.dto.request; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatMessageResponse.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatMessageResponse.java similarity index 72% rename from src/main/java/com/dnd/gongmuin/chat/dto/response/ChatMessageResponse.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatMessageResponse.java index 368fb1c7..ab02a180 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatMessageResponse.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatMessageResponse.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.response; +package com.dnd.gongmuin.chatroom.dto.response; public record ChatMessageResponse( Long senderId, diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomDetailResponse.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomDetailResponse.java similarity index 77% rename from src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomDetailResponse.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomDetailResponse.java index 390acf66..52502c5d 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomDetailResponse.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomDetailResponse.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.response; +package com.dnd.gongmuin.chatroom.dto.response; import com.dnd.gongmuin.question_post.dto.response.MemberInfo; @@ -7,7 +7,6 @@ public record ChatRoomDetailResponse( String targetJobGroup, String title, MemberInfo receiverInfo, - String chatStatus, boolean isInquirer ) { } diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomInfo.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomInfo.java similarity index 91% rename from src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomInfo.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomInfo.java index 52bc26f3..885c37d6 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomInfo.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomInfo.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.response; +package com.dnd.gongmuin.chatroom.dto.response; import com.dnd.gongmuin.member.domain.JobGroup; import com.querydsl.core.annotations.QueryProjection; diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomSimpleResponse.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomSimpleResponse.java similarity index 82% rename from src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomSimpleResponse.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomSimpleResponse.java index 136b893c..aa7fa435 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomSimpleResponse.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/ChatRoomSimpleResponse.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.response; +package com.dnd.gongmuin.chatroom.dto.response; import com.dnd.gongmuin.question_post.dto.response.MemberInfo; diff --git a/src/main/java/com/dnd/gongmuin/chat/dto/response/LatestChatMessage.java b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/LatestChatMessage.java similarity index 75% rename from src/main/java/com/dnd/gongmuin/chat/dto/response/LatestChatMessage.java rename to src/main/java/com/dnd/gongmuin/chatroom/dto/response/LatestChatMessage.java index 57934805..cd73a5af 100644 --- a/src/main/java/com/dnd/gongmuin/chat/dto/response/LatestChatMessage.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/dto/response/LatestChatMessage.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.dto.response; +package com.dnd.gongmuin.chatroom.dto.response; import java.time.LocalDateTime; diff --git a/src/main/java/com/dnd/gongmuin/chat/exception/ChatErrorCode.java b/src/main/java/com/dnd/gongmuin/chatroom/exception/ChatErrorCode.java similarity index 94% rename from src/main/java/com/dnd/gongmuin/chat/exception/ChatErrorCode.java rename to src/main/java/com/dnd/gongmuin/chatroom/exception/ChatErrorCode.java index 11635b5d..dca2559f 100644 --- a/src/main/java/com/dnd/gongmuin/chat/exception/ChatErrorCode.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/exception/ChatErrorCode.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.exception; +package com.dnd.gongmuin.chatroom.exception; import com.dnd.gongmuin.common.exception.ErrorCode; diff --git a/src/main/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepository.java b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatMessageQueryRepository.java similarity index 93% rename from src/main/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepository.java rename to src/main/java/com/dnd/gongmuin/chatroom/repository/ChatMessageQueryRepository.java index 50c7f193..7edf46a0 100644 --- a/src/main/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepository.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatMessageQueryRepository.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.chat.repository; +package com.dnd.gongmuin.chatroom.repository; import java.util.List; @@ -10,7 +10,7 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.stereotype.Component; -import com.dnd.gongmuin.chat.dto.response.LatestChatMessage; +import com.dnd.gongmuin.chatroom.dto.response.LatestChatMessage; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/dnd/gongmuin/chat/repository/ChatMessageRepository.java b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatMessageRepository.java similarity index 80% rename from src/main/java/com/dnd/gongmuin/chat/repository/ChatMessageRepository.java rename to src/main/java/com/dnd/gongmuin/chatroom/repository/ChatMessageRepository.java index 1572e546..4a5f1c05 100644 --- a/src/main/java/com/dnd/gongmuin/chat/repository/ChatMessageRepository.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatMessageRepository.java @@ -1,11 +1,11 @@ -package com.dnd.gongmuin.chat.repository; +package com.dnd.gongmuin.chatroom.repository; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; -import com.dnd.gongmuin.chat.domain.ChatMessage; +import com.dnd.gongmuin.chatroom.domain.ChatMessage; @Repository public interface ChatMessageRepository extends MongoRepository { diff --git a/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepository.java b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepository.java new file mode 100644 index 00000000..aedb80ea --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepository.java @@ -0,0 +1,12 @@ +package com.dnd.gongmuin.chatroom.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.member.domain.Member; + +public interface ChatRoomQueryRepository { + + Slice getChatRoomsByMember(Member member, Pageable pageable); +} diff --git a/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepositoryImpl.java b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepositoryImpl.java new file mode 100644 index 00000000..39988f07 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomQueryRepositoryImpl.java @@ -0,0 +1,65 @@ +package com.dnd.gongmuin.chatroom.repository; + +import static com.dnd.gongmuin.chatroom.domain.QChatRoom.*; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.chatroom.dto.response.QChatRoomInfo; +import com.dnd.gongmuin.member.domain.Member; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ChatRoomQueryRepositoryImpl implements ChatRoomQueryRepository { + + private final JPAQueryFactory queryFactory; + + public Slice getChatRoomsByMember( + Member member, + Pageable pageable + ) { + List content = queryFactory + .select(new QChatRoomInfo( + chatRoom.id, + new CaseBuilder() + .when(chatRoom.inquirer.id.eq(member.getId())) + .then(chatRoom.answerer.id) + .otherwise(chatRoom.inquirer.id), + + new CaseBuilder() + .when(chatRoom.inquirer.id.eq(member.getId())) + .then(chatRoom.answerer.nickname) + .otherwise(chatRoom.inquirer.nickname), + new CaseBuilder() + .when(chatRoom.inquirer.id.eq(member.getId())) + .then(chatRoom.answerer.jobGroup) + .otherwise(chatRoom.inquirer.jobGroup), + new CaseBuilder() + .when(chatRoom.inquirer.id.eq(member.getId())) + .then(chatRoom.answerer.profileImageNo) + .otherwise(chatRoom.inquirer.profileImageNo) + )) + .from(chatRoom) + .where(chatRoom.inquirer.id.eq(member.getId()) + .or(chatRoom.answerer.id.eq(member.getId()))) + .fetch(); + + boolean hasNext = hasNext(pageable.getPageSize(), content); + return new SliceImpl<>(content, pageable, hasNext); + } + + private boolean hasNext(int pageSize, List items) { + if (items.size() <= pageSize) { + return false; + } + items.remove(pageSize); + return true; + } +} diff --git a/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomRepository.java b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomRepository.java similarity index 63% rename from src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomRepository.java rename to src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomRepository.java index 8499787d..ddd13b13 100644 --- a/src/main/java/com/dnd/gongmuin/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/repository/ChatRoomRepository.java @@ -1,8 +1,8 @@ -package com.dnd.gongmuin.chat.repository; +package com.dnd.gongmuin.chatroom.repository; import org.springframework.data.jpa.repository.JpaRepository; -import com.dnd.gongmuin.chat.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; public interface ChatRoomRepository extends JpaRepository, ChatRoomQueryRepository { } diff --git a/src/main/java/com/dnd/gongmuin/chat/service/ChatMessageService.java b/src/main/java/com/dnd/gongmuin/chatroom/service/ChatMessageService.java similarity index 68% rename from src/main/java/com/dnd/gongmuin/chat/service/ChatMessageService.java rename to src/main/java/com/dnd/gongmuin/chatroom/service/ChatMessageService.java index e9b248bf..9be22443 100644 --- a/src/main/java/com/dnd/gongmuin/chat/service/ChatMessageService.java +++ b/src/main/java/com/dnd/gongmuin/chatroom/service/ChatMessageService.java @@ -1,13 +1,13 @@ -package com.dnd.gongmuin.chat.service; +package com.dnd.gongmuin.chatroom.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.dnd.gongmuin.chat.domain.ChatMessage; -import com.dnd.gongmuin.chat.dto.ChatMessageMapper; -import com.dnd.gongmuin.chat.dto.request.ChatMessageRequest; -import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse; -import com.dnd.gongmuin.chat.repository.ChatMessageRepository; +import com.dnd.gongmuin.chatroom.domain.ChatMessage; +import com.dnd.gongmuin.chatroom.dto.ChatMessageMapper; +import com.dnd.gongmuin.chatroom.dto.request.ChatMessageRequest; +import com.dnd.gongmuin.chatroom.dto.response.ChatMessageResponse; +import com.dnd.gongmuin.chatroom.repository.ChatMessageRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/dnd/gongmuin/chatroom/service/ChatRoomService.java b/src/main/java/com/dnd/gongmuin/chatroom/service/ChatRoomService.java new file mode 100644 index 00000000..e91c7925 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/chatroom/service/ChatRoomService.java @@ -0,0 +1,113 @@ +package com.dnd.gongmuin.chatroom.service; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.dto.ChatMessageMapper; +import com.dnd.gongmuin.chatroom.dto.ChatRoomMapper; +import com.dnd.gongmuin.chatroom.dto.response.ChatMessageResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomDetailResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomSimpleResponse; +import com.dnd.gongmuin.chatroom.dto.response.LatestChatMessage; +import com.dnd.gongmuin.chatroom.exception.ChatErrorCode; +import com.dnd.gongmuin.chatroom.repository.ChatMessageQueryRepository; +import com.dnd.gongmuin.chatroom.repository.ChatMessageRepository; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; +import com.dnd.gongmuin.common.dto.PageMapper; +import com.dnd.gongmuin.common.dto.PageResponse; +import com.dnd.gongmuin.common.exception.runtime.NotFoundException; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; +import com.dnd.gongmuin.member.domain.Member; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatRoomService { + + private final ChatMessageRepository chatMessageRepository; + private final ChatMessageQueryRepository chatMessageQueryRepository; + private final ChatRoomRepository chatRoomRepository; + + @Transactional(readOnly = true) + public PageResponse getChatMessages(Long chatRoomId, Pageable pageable) { + Slice responsePage = chatMessageRepository + .findByChatRoomIdOrderByCreatedAtDesc(chatRoomId, pageable) + .map(ChatMessageMapper::toChatMessageResponse); + return PageMapper.toPageResponse(responsePage); + } + + @Transactional(readOnly = true) + public PageResponse getChatRoomsByMember(Member member, Pageable pageable) { + // 회원 채팅방 정보 가져오기 + Slice chatRoomInfos = chatRoomRepository.getChatRoomsByMember( + member, pageable + ); + + // chatRoomId 리스트 추출 + List chatRoomIds = chatRoomInfos.stream() + .map(ChatRoomInfo::chatRoomId) + .toList(); + + // 각 채팅방 최근 메시지 가져오기 + List latestChatMessages + = chatMessageQueryRepository.findLatestChatByChatRoomIds(chatRoomIds); + + // 두 객체 합쳐서 하나의 DTO로 반환 + List responses = getChatRoomSimpleResponses(latestChatMessages, + chatRoomInfos); + + // PageResponse 객체 생성 + return new PageResponse<>(responses, responses.size(), chatRoomInfos.hasNext()); + } + + @Transactional(readOnly = true) + public ChatRoomDetailResponse getChatRoomById(Long chatRoomId, Member member) { + ChatRoom chatRoom = getChatRoomById(chatRoomId); + Member chatPartner = getChatPartner(member, chatRoom); + return ChatRoomMapper.toChatRoomDetailResponse(chatRoom, chatPartner); + } + + private List getChatRoomSimpleResponses(List latestChatMessages, + Slice chatRoomInfos) { + // -> 순서 보장 x + Map messageMap = latestChatMessages.stream() + .collect(Collectors.toMap(LatestChatMessage::chatRoomId, message -> message)); + + // 최신순 정렬 및 변환 + return chatRoomInfos.stream() + .sorted( + Comparator.comparing( + (ChatRoomInfo info) -> messageMap.get(info.chatRoomId()).createdAt() + ).reversed()) + .map(chatRoomInfo -> { + LatestChatMessage latestMessage = messageMap.get(chatRoomInfo.chatRoomId()); + return ChatRoomMapper.toChatRoomSimpleResponse( + chatRoomInfo, latestMessage + ); + }).toList(); + } + + private ChatRoom getChatRoomById(Long id) { + return chatRoomRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ChatErrorCode.NOT_FOUND_CHAT_ROOM)); + } + + private Member getChatPartner(Member member, ChatRoom chatRoom) { + if (member.isEqualMember(chatRoom.getAnswerer().getId())) { + return chatRoom.getInquirer(); + } else if (member.isEqualMember(chatRoom.getInquirer().getId())) { + return chatRoom.getAnswerer(); + } + throw new ValidationException(ChatErrorCode.UNAUTHORIZED_CHAT_ROOM); + } +} diff --git a/src/main/java/com/dnd/gongmuin/member/domain/Member.java b/src/main/java/com/dnd/gongmuin/member/domain/Member.java index 87b1ce57..aa7cd70c 100644 --- a/src/main/java/com/dnd/gongmuin/member/domain/Member.java +++ b/src/main/java/com/dnd/gongmuin/member/domain/Member.java @@ -133,6 +133,10 @@ public void updateProfile(String nickname, JobGroup jobGroup, JobCategory jobCat this.jobCategory = jobCategory; } + public boolean isEqualMember(Long id) { + return this.id.equals(id); + } + private int setRandomNumber() { Random random = new Random(); return random.nextInt(1, 10); diff --git a/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java b/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java index b0b9a4ee..2a8bf2eb 100644 --- a/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/chat/controller/ChatRoomControllerTest.java @@ -1,6 +1,5 @@ package com.dnd.gongmuin.chat.controller; -import static org.springframework.http.MediaType.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -13,12 +12,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; -import com.dnd.gongmuin.chat.domain.ChatMessage; -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.domain.ChatStatus; -import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest; -import com.dnd.gongmuin.chat.repository.ChatMessageRepository; -import com.dnd.gongmuin.chat.repository.ChatRoomRepository; +import com.dnd.gongmuin.chatroom.domain.ChatMessage; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.repository.ChatMessageRepository; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; import com.dnd.gongmuin.common.fixture.ChatMessageFixture; import com.dnd.gongmuin.common.fixture.ChatRoomFixture; import com.dnd.gongmuin.common.fixture.MemberFixture; @@ -30,11 +27,9 @@ import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; -@DisplayName("[ChatMessage 통합 테스트]") +@DisplayName("[채팅방 통합 테스트]") class ChatRoomControllerTest extends ApiTestSupport { - private static final int CHAT_REWARD = 2000; - @Autowired private ChatMessageRepository chatMessageRepository; @@ -76,28 +71,6 @@ void getChatMessages() throws Exception { .andExpect(jsonPath("$.content[0].isRead").value(chatMessages.get(0).getIsRead())); } - @DisplayName("[채팅방을 생성할 수 있다.]") - @Test - void createChatRoom() throws Exception { - Member answerer = memberRepository.save(MemberFixture.member4()); - QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember)); - CreateChatRoomRequest request = new CreateChatRoomRequest(questionPost.getId(), answerer.getId()); - - mockMvc.perform(post("/api/chat-rooms") - .cookie(accessToken) - .content(toJson(request)) - .contentType(APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.credit").value(loginMember.getCredit() - CHAT_REWARD)) - .andExpect(jsonPath("$.questionPostId").value(questionPost.getId())) - .andExpect(jsonPath("$.targetJobGroup").value(questionPost.getJobGroup().getLabel())) - .andExpect(jsonPath("$.title").value(questionPost.getTitle())) - .andExpect(jsonPath("$.receiverInfo.memberId").value(answerer.getId())) - .andExpect(jsonPath("$.receiverInfo.nickname").value(answerer.getNickname())) - .andExpect(jsonPath("$.receiverInfo.memberJobGroup").value(answerer.getJobGroup().getLabel())) - .andExpect(jsonPath("$.receiverInfo.profileImageNo").value(answerer.getProfileImageNo())); - } - @DisplayName("[회원의 채팅방 목록을 조회할 수 있다.]") @Test void getChatRoomsByMember() throws Exception { @@ -111,13 +84,13 @@ void getChatRoomsByMember() throws Exception { ) ); ChatRoom chatRoom1 = chatRoomRepository.save( - ChatRoomFixture.acceptedChatRoom(questionPosts.get(0), member1, loginMember)); + ChatRoomFixture.chatRoom(questionPosts.get(0), member1, loginMember)); ChatRoom chatRoom2 = chatRoomRepository.save( - ChatRoomFixture.acceptedChatRoom(questionPosts.get(0), member2, loginMember)); + ChatRoomFixture.chatRoom(questionPosts.get(0), member2, loginMember)); ChatRoom chatRoom3 = chatRoomRepository.save( - ChatRoomFixture.acceptedChatRoom(questionPosts.get(1), loginMember, member1)); + ChatRoomFixture.chatRoom(questionPosts.get(1), loginMember, member1)); ChatRoom unrelatedChatroom = chatRoomRepository.save( - ChatRoomFixture.acceptedChatRoom(questionPosts.get(1), member2, member1)); + ChatRoomFixture.chatRoom(questionPosts.get(1), member2, member1)); chatMessageRepository.saveAll( List.of( @@ -156,55 +129,6 @@ void getChatRoomsByMember() throws Exception { .andDo(MockMvcResultHandlers.print()); } - @DisplayName("[회원의 채팅 요청 목록을 조회할 수 있다.]") - @Test - void getChatProposalsByMember() throws Exception { - //given - Member member1 = memberRepository.save(MemberFixture.member4()); - Member member2 = memberRepository.save(MemberFixture.member5()); - List questionPosts = questionPostRepository.saveAll( - List.of( - questionPostRepository.save(QuestionPostFixture.questionPost(member1)), - questionPostRepository.save(QuestionPostFixture.questionPost(member2)) - ) - ); - ChatRoom chatRoom1 = chatRoomRepository.save( - ChatRoomFixture.chatRoom(questionPosts.get(0), member1, loginMember)); - ChatRoom chatRoom2 = chatRoomRepository.save( - ChatRoomFixture.chatRoom(questionPosts.get(1), loginMember, member1)); - ChatRoom unrelatedChatroom = chatRoomRepository.save( - ChatRoomFixture.chatRoom(questionPosts.get(1), member2, member1)); - - chatMessageRepository.saveAll( - List.of( - chatMessageRepository.save( - ChatMessageFixture.chatMessage(chatRoom1.getId(), "11", LocalDateTime.now())), - chatMessageRepository.save( - ChatMessageFixture.chatMessage(chatRoom2.getId(), "21", LocalDateTime.now())), - chatMessageRepository.save( - ChatMessageFixture.chatMessage(unrelatedChatroom.getId(), "31", LocalDateTime.now())) - ) - ); - - // when & then - mockMvc.perform(get("/api/chat-rooms/proposals") - .cookie(accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.size").value(2)) - .andExpect(jsonPath("$.content[0].chatRoomId").value(chatRoom2.getId())) - .andExpect(jsonPath("$.content[0].latestMessage").value("21")) - .andExpect(jsonPath("$.content[0].chatPartner.memberId").value(member1.getId())) - .andExpect(jsonPath("$.content[0].isInquirer").value(true)) - .andExpect(jsonPath("$.content[0].chatStatus").value(ChatStatus.PENDING.getLabel())) - - .andExpect(jsonPath("$.content[1].chatRoomId").value(chatRoom1.getId())) - .andExpect(jsonPath("$.content[1].latestMessage").value("11")) - .andExpect(jsonPath("$.content[1].chatPartner.memberId").value(member1.getId())) - .andExpect(jsonPath("$.content[1].isInquirer").value(false)) - .andExpect(jsonPath("$.content[1].chatStatus").value(ChatStatus.PENDING.getLabel())) - .andDo(MockMvcResultHandlers.print()); - } - @DisplayName("[채팅방 아이디로 채팅방을 상세조회할 수 있다.]") @Test void getChatRoomById() throws Exception { @@ -224,32 +148,4 @@ void getChatRoomById() throws Exception { .andExpect(jsonPath("$.receiverInfo.profileImageNo").value(inquirer.getProfileImageNo())) .andExpect(jsonPath("$.isInquirer").value(false)); } - - @DisplayName("[답변자가 채팅 요청을 수락할 수 있다.]") - @Test - void acceptChatRoom() throws Exception { - Member inquirer = memberRepository.save(MemberFixture.member4()); - QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(inquirer)); - ChatRoom chatRoom = chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, inquirer, loginMember)); - int previousAnswererCredit = chatRoom.getAnswerer().getCredit(); - - mockMvc.perform(patch("/api/chat-rooms/{chatRoomId}/accept", chatRoom.getId()) - .cookie(accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.chatStatus").value(ChatStatus.ACCEPTED.getLabel())) - .andExpect(jsonPath("$.credit").value(previousAnswererCredit + CHAT_REWARD)); - } - - @DisplayName("[답변자가 채팅 요청을 거절할 수 있다.]") - @Test - void rejectChatRoom() throws Exception { - Member inquirer = memberRepository.save(MemberFixture.member4()); - QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(inquirer)); - ChatRoom chatRoom = chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, inquirer, loginMember)); - - mockMvc.perform(patch("/api/chat-rooms/{chatRoomId}/reject", chatRoom.getId()) - .cookie(accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.chatStatus").value(ChatStatus.REJECTED.getLabel())); - } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepositoryTest.java b/src/test/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepositoryTest.java index 085ccf7a..b50db93f 100644 --- a/src/test/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepositoryTest.java +++ b/src/test/java/com/dnd/gongmuin/chat/repository/ChatMessageQueryRepositoryTest.java @@ -12,7 +12,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import com.dnd.gongmuin.chat.dto.response.LatestChatMessage; +import com.dnd.gongmuin.chatroom.dto.response.LatestChatMessage; +import com.dnd.gongmuin.chatroom.repository.ChatMessageQueryRepository; +import com.dnd.gongmuin.chatroom.repository.ChatMessageRepository; import com.dnd.gongmuin.common.fixture.ChatMessageFixture; import com.dnd.gongmuin.common.support.TestContainerSupport; diff --git a/src/test/java/com/dnd/gongmuin/chat/repository/ChatRoomRepositoryTest.java b/src/test/java/com/dnd/gongmuin/chat/repository/ChatRoomRepositoryTest.java index 45b34212..a703b3e3 100644 --- a/src/test/java/com/dnd/gongmuin/chat/repository/ChatRoomRepositoryTest.java +++ b/src/test/java/com/dnd/gongmuin/chat/repository/ChatRoomRepositoryTest.java @@ -1,9 +1,7 @@ package com.dnd.gongmuin.chat.repository; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; -import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -11,12 +9,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; -import org.springframework.test.util.ReflectionTestUtils; -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.domain.ChatStatus; -import com.dnd.gongmuin.chat.dto.response.ChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; import com.dnd.gongmuin.common.fixture.ChatRoomFixture; import com.dnd.gongmuin.common.fixture.MemberFixture; import com.dnd.gongmuin.common.fixture.QuestionPostFixture; @@ -54,9 +50,9 @@ void getChatRoomsByMember() { Member answerer = memberRepository.save(MemberFixture.member()); QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(questioner)); List chatRooms = chatRoomRepository.saveAll(List.of( - chatRoomRepository.save(ChatRoomFixture.acceptedChatRoom(questionPost, questioner, answerer)), - chatRoomRepository.save(ChatRoomFixture.acceptedChatRoom(questionPost, questioner, target)), - chatRoomRepository.save(ChatRoomFixture.acceptedChatRoom(questionPost, target, answerer)) + chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, questioner, answerer)), + chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, questioner, target)), + chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, target, answerer)) )); //when @@ -72,61 +68,4 @@ void getChatRoomsByMember() { () -> assertThat(chatRoomInfos.get(1).partnerId()).isEqualTo(answerer.getId()) ); } - - @DisplayName("회원의 채팅 요청 목록을 조회할 수 있다.") - @Test - void getChatProposalsByMember() { - //given - Member questioner = memberRepository.save(MemberFixture.member()); - Member target = memberRepository.save(MemberFixture.member()); - Member answerer = memberRepository.save(MemberFixture.member()); - QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(questioner)); - List chatRooms = chatRoomRepository.saveAll(List.of( - chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, questioner, answerer)), - chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, questioner, target)), - chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, target, answerer)) - )); - - //when - List chatProposalInfos = chatRoomRepository.getChatProposalsByMember(target, pageRequest) - .getContent(); - - //then - Assertions.assertAll( - () -> assertThat(chatProposalInfos).hasSize(2), - () -> assertThat(chatProposalInfos.get(0).chatRoomId()).isEqualTo(chatRooms.get(1).getId()), - () -> assertThat(chatProposalInfos.get(0).partnerId()).isEqualTo(questioner.getId()), - () -> assertThat(chatProposalInfos.get(1).chatRoomId()).isEqualTo(chatRooms.get(2).getId()), - () -> assertThat(chatProposalInfos.get(1).partnerId()).isEqualTo(answerer.getId()) - ); - } - - @DisplayName("요청중인 채팅방이 일주일이 지나면, 채팅방 상태를 거절함으로 바꾼다.") - @Test - void updateChatRoomStatusRejected() { - //given - Member questioner = memberRepository.save(MemberFixture.member()); - Member answerer = memberRepository.save(MemberFixture.member()); - QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(questioner)); - - List chatRooms = chatRoomRepository.saveAll(List.of( - chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, questioner, answerer)), - chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, questioner, answerer)) - )); - ReflectionTestUtils.setField(chatRooms.get(0), "createdAt", LocalDateTime.now().minusWeeks(1)); - - //when - chatRoomRepository.updateChatRoomStatusRejected(); - - em.flush(); - em.clear(); - - //then - ChatRoom chatRoom1 = chatRoomRepository.findById(chatRooms.get(0).getId()).orElseThrow(); - ChatRoom chatRoom2 = chatRoomRepository.findById(chatRooms.get(1).getId()).orElseThrow(); - assertAll( - () -> assertThat(chatRoom1.getStatus()).isEqualTo(ChatStatus.REJECTED), - () -> assertThat(chatRoom2.getStatus()).isEqualTo(ChatStatus.PENDING) - ); - } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/chat/service/ChatRoomServiceTest.java b/src/test/java/com/dnd/gongmuin/chat/service/ChatRoomServiceTest.java index 1775ddb6..7b84e5d6 100644 --- a/src/test/java/com/dnd/gongmuin/chat/service/ChatRoomServiceTest.java +++ b/src/test/java/com/dnd/gongmuin/chat/service/ChatRoomServiceTest.java @@ -14,50 +14,33 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.SliceImpl; -import org.springframework.test.util.ReflectionTestUtils; -import com.dnd.gongmuin.chat.domain.ChatMessage; -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.domain.ChatStatus; -import com.dnd.gongmuin.chat.domain.MessageType; -import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest; -import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse; -import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse; -import com.dnd.gongmuin.chat.dto.response.ChatProposalInfo; -import com.dnd.gongmuin.chat.dto.response.ChatProposalResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse; -import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo; -import com.dnd.gongmuin.chat.dto.response.ChatRoomSimpleResponse; -import com.dnd.gongmuin.chat.dto.response.CreateChatRoomResponse; -import com.dnd.gongmuin.chat.dto.response.LatestChatMessage; -import com.dnd.gongmuin.chat.dto.response.RejectChatResponse; -import com.dnd.gongmuin.chat.exception.ChatErrorCode; -import com.dnd.gongmuin.chat.repository.ChatMessageQueryRepository; -import com.dnd.gongmuin.chat.repository.ChatMessageRepository; -import com.dnd.gongmuin.chat.repository.ChatRoomRepository; +import com.dnd.gongmuin.chatroom.domain.ChatMessage; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.dto.response.ChatMessageResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomDetailResponse; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomInfo; +import com.dnd.gongmuin.chatroom.dto.response.ChatRoomSimpleResponse; +import com.dnd.gongmuin.chatroom.dto.response.LatestChatMessage; +import com.dnd.gongmuin.chatroom.exception.ChatErrorCode; +import com.dnd.gongmuin.chatroom.repository.ChatMessageQueryRepository; +import com.dnd.gongmuin.chatroom.repository.ChatMessageRepository; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; +import com.dnd.gongmuin.chatroom.service.ChatRoomService; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.common.fixture.ChatMessageFixture; import com.dnd.gongmuin.common.fixture.ChatRoomFixture; import com.dnd.gongmuin.common.fixture.MemberFixture; import com.dnd.gongmuin.common.fixture.QuestionPostFixture; -import com.dnd.gongmuin.credit_history.domain.CreditType; -import com.dnd.gongmuin.credit_history.service.CreditHistoryService; import com.dnd.gongmuin.member.domain.Member; -import com.dnd.gongmuin.member.exception.MemberErrorCode; -import com.dnd.gongmuin.member.repository.MemberRepository; -import com.dnd.gongmuin.notification.dto.NotificationEvent; import com.dnd.gongmuin.question_post.domain.QuestionPost; -import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; @DisplayName("[채팅방 서비스 단위 테스트]") @ExtendWith(MockitoExtension.class) class ChatRoomServiceTest { - private static final int CHAT_REWARD = 2000; - private static final String REQUEST_MESSAGE_POSTFIX = "님이 채팅을 요청하셨습니다."; private final PageRequest pageRequest = PageRequest.of(0, 5); @Mock private ChatMessageRepository chatMessageRepository; @@ -68,18 +51,6 @@ class ChatRoomServiceTest { @Mock private ChatRoomRepository chatRoomRepository; - @Mock - private MemberRepository memberRepository; - - @Mock - private QuestionPostRepository questionPostRepository; - - @Mock - private ApplicationEventPublisher eventPublisher; - - @Mock - private CreditHistoryService creditHistoryService; - @InjectMocks private ChatRoomService chatRoomService; @@ -100,99 +71,6 @@ void getChatMessages() { ); } - @DisplayName("[요청자가 채팅방을 생성할 수 있다.]") - @Test - void createChatRoom() { - //given - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - CreateChatRoomRequest request = new CreateChatRoomRequest( - questionPost.getId(), - answerer.getId() - ); - ChatRoom chatRoom = ChatRoomFixture.chatRoom(1L, questionPost, inquirer, answerer); - - given(questionPostRepository.findById(questionPost.getId())) - .willReturn(Optional.of(questionPost)); - given(memberRepository.findById(answerer.getId())) - .willReturn(Optional.of(answerer)); - given(chatRoomRepository.save(any(ChatRoom.class))) - .willReturn(chatRoom); - given(chatMessageRepository.save(any(ChatMessage.class))) - .willReturn( - ChatMessage.of(inquirer + REQUEST_MESSAGE_POSTFIX, chatRoom.getId(), inquirer.getId(), MessageType.TEXT) - ); - - //when - CreateChatRoomResponse response = chatRoomService.createChatRoom(request, inquirer); - - //then - assertAll( - () -> assertThat(response.questionPostId()).isEqualTo(request.questionPostId()), - () -> assertThat(response.receiverInfo().memberId()).isEqualTo(request.answererId()) - ); - } - - @DisplayName("[요청자가 채팅방을 생성 시 생성 알림이 발행된다.]") - @Test - void createChatRoomWithEventPublish() { - //given - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - CreateChatRoomRequest request = new CreateChatRoomRequest( - questionPost.getId(), - answerer.getId() - ); - ChatRoom chatRoom = ChatRoomFixture.chatRoom(1L, questionPost, inquirer, answerer); - - given(questionPostRepository.findById(questionPost.getId())) - .willReturn(Optional.of(questionPost)); - given(memberRepository.findById(answerer.getId())) - .willReturn(Optional.of(answerer)); - given(chatRoomRepository.save(any(ChatRoom.class))) - .willReturn(chatRoom); - given(chatMessageRepository.save(any(ChatMessage.class))) - .willReturn( - ChatMessage.of(inquirer + REQUEST_MESSAGE_POSTFIX, chatRoom.getId(), inquirer.getId(), MessageType.TEXT) - ); - - //when - CreateChatRoomResponse response = chatRoomService.createChatRoom(request, inquirer); - - //then - assertAll( - () -> assertThat(response.questionPostId()).isEqualTo(request.questionPostId()), - () -> assertThat(response.receiverInfo().memberId()).isEqualTo(request.answererId()), - () -> verify(eventPublisher, times(1)).publishEvent(any(NotificationEvent.class)) - ); - } - - @DisplayName("[요청자의 크레딧이 2000미만이면 채팅방을 생성할 수 없다.]") - @Test - void createChatRoom_fail() { - //given - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - ReflectionTestUtils.setField(inquirer, "credit", 1999); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - CreateChatRoomRequest request = new CreateChatRoomRequest( - questionPost.getId(), - answerer.getId() - ); - - given(questionPostRepository.findById(questionPost.getId())) - .willReturn(Optional.of(questionPost)); - given(memberRepository.findById(answerer.getId())) - .willReturn(Optional.of(answerer)); - - //when & then - assertThatThrownBy(() -> chatRoomService.createChatRoom(request, inquirer)) - .isInstanceOf(ValidationException.class) - .hasMessageContaining(MemberErrorCode.NOT_ENOUGH_CREDIT.getMessage()); - } - @DisplayName("[회원이 속한 채팅방 목록을 조회할 수 있다.]") @Test void getChatRoomsByMember() { @@ -229,42 +107,6 @@ void getChatRoomsByMember() { ); } - @DisplayName("[회원이 속한 채팅 요청 목록을 조회할 수 있다.]") - @Test - void getChatProposalsByMember() { - //given - Long chatRoomId = 1L; - Member targetMember = MemberFixture.member(1L); - Member partner = MemberFixture.member(2L); - ChatProposalInfo chatProposalInfo = new ChatProposalInfo( - chatRoomId, ChatStatus.PENDING, true, partner.getId(), - partner.getNickname(), partner.getJobGroup(), partner.getProfileImageNo() - ); - LatestChatMessage latestChatMessage = new LatestChatMessage( - chatRoomId, "와", "텍스트", LocalDateTime.now() - ); - - given(chatRoomRepository.getChatProposalsByMember(targetMember, pageRequest)) - .willReturn(new SliceImpl<>(List.of(chatProposalInfo), pageRequest, false)); - given(chatMessageQueryRepository.findLatestChatByChatRoomIds(List.of(chatRoomId))) - .willReturn(List.of(latestChatMessage)); - - //when - List response = chatRoomService.getChatProposalsByMember( - targetMember, pageRequest).content(); - - //then - assertAll( - () -> assertThat(response).hasSize(1), - () -> assertThat(response.get(0).chatRoomId()) - .isEqualTo(chatRoomId), - () -> assertThat(response.get(0).chatPartner().memberId()) - .isEqualTo(partner.getId()), - () -> assertThat(response.get(0).latestMessage()) - .isEqualTo(latestChatMessage.content()) - ); - } - @DisplayName("[요청자가 채팅방 아이디로 채팅방을 조회할 수 있다.]") @Test void getChatRoomById_Inquirer() { @@ -336,121 +178,4 @@ void getChatRoomById_Unauthorized() { .isInstanceOf(ValidationException.class) .hasMessageContaining(ChatErrorCode.UNAUTHORIZED_CHAT_ROOM.getMessage()); } - - @DisplayName("[답변자가 채팅 요청을 수락할 수 있다.]") - @Test - void acceptChat() { - //given - Long chatRoomId = 1L; - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - int previousCredit = answerer.getCredit(); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - ChatRoom chatRoom = ChatRoomFixture.chatRoom(questionPost, inquirer, answerer); - - given(chatRoomRepository.findById(chatRoomId)) - .willReturn(Optional.of(chatRoom)); - - //when - AcceptChatResponse response = chatRoomService.acceptChat(chatRoomId, answerer); - - //then - assertAll( - () -> assertThat(response.chatStatus()) - .isEqualTo(ChatStatus.ACCEPTED.getLabel()), - () -> assertThat(response.credit()) - .isEqualTo(previousCredit + CHAT_REWARD) - ); - } - - @DisplayName("[답변자가 채팅 요청을 수락할 때 채팅 수락 알림이 발행된다.]") - @Test - void acceptChatWithEventPublish() { - //given - Long chatRoomId = 1L; - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - int previousCredit = answerer.getCredit(); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - ChatRoom chatRoom = ChatRoomFixture.chatRoom(questionPost, inquirer, answerer); - - given(chatRoomRepository.findById(chatRoomId)) - .willReturn(Optional.of(chatRoom)); - - //when - AcceptChatResponse response = chatRoomService.acceptChat(chatRoomId, answerer); - - //then - assertAll( - () -> assertThat(response.chatStatus()) - .isEqualTo(ChatStatus.ACCEPTED.getLabel()), - () -> assertThat(response.credit()) - .isEqualTo(previousCredit + CHAT_REWARD), - () -> verify(eventPublisher, times(1)).publishEvent(any(NotificationEvent.class)) - ); - } - - @DisplayName("[답변자가 채팅 요청을 거절할 수 있다.]") - @Test - void rejectChat() { - //given - Long chatRoomId = 1L; - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - ChatRoom chatRoom = ChatRoomFixture.chatRoom(questionPost, inquirer, answerer); - - given(chatRoomRepository.findById(chatRoomId)) - .willReturn(Optional.of(chatRoom)); - - //when - RejectChatResponse response = chatRoomService.rejectChat(chatRoomId, answerer); - - //then - assertThat(response.chatStatus()) - .isEqualTo(ChatStatus.REJECTED.getLabel()); - } - - @DisplayName("[답변자가 채팅 요청을 거절할 때 채팅 거절 알림이 발행된다.]") - @Test - void rejectChatWithEventPublish() { - //given - Long chatRoomId = 1L; - Member inquirer = MemberFixture.member(1L); - Member answerer = MemberFixture.member(2L); - QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); - ChatRoom chatRoom = ChatRoomFixture.chatRoom(questionPost, inquirer, answerer); - - given(chatRoomRepository.findById(chatRoomId)) - .willReturn(Optional.of(chatRoom)); - - //when - RejectChatResponse response = chatRoomService.rejectChat(chatRoomId, answerer); - - //then - assertAll( - () -> assertThat(response.chatStatus()).isEqualTo(ChatStatus.REJECTED.getLabel()), - () -> verify(eventPublisher, times(1)).publishEvent(any(NotificationEvent.class)) - ); - } - - @DisplayName("일주일이 지난 요청에 경우 자동으로 거절하고, 요청자에게 크레딧을 반환한다.") - @Test - void rejectChatAuto() { - // given - List rejectedInquirerIds = List.of(1L, 2L); - given(chatRoomRepository.getAutoRejectedInquirerIds()) - .willReturn(rejectedInquirerIds); - - // when - chatRoomService.rejectChatAuto(); - - // then - verify(chatRoomRepository).getAutoRejectedInquirerIds(); - verify(chatRoomRepository).updateChatRoomStatusRejected(); - verify(memberRepository).refundInMemberIds(rejectedInquirerIds, CHAT_REWARD); - verify(creditHistoryService).saveCreditHistoryInMemberIds( - rejectedInquirerIds, CreditType.CHAT_REFUND, CHAT_REWARD - ); - } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryControllerTest.java b/src/test/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryControllerTest.java new file mode 100644 index 00000000..8d41caf9 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/chat_inquiry/controller/ChatInquiryControllerTest.java @@ -0,0 +1,148 @@ +package com.dnd.gongmuin.chat_inquiry.controller; + +import static org.springframework.http.MediaType.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; +import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryRequest; +import com.dnd.gongmuin.chat_inquiry.repository.ChatInquiryRepository; +import com.dnd.gongmuin.chatroom.repository.ChatMessageRepository; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; +import com.dnd.gongmuin.common.fixture.ChatInquiryFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.common.support.ApiTestSupport; +import com.dnd.gongmuin.credit_history.repository.CreditHistoryRepository; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.repository.MemberRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +@DisplayName("[채팅 요청 통합 테스트]") +class ChatInquiryControllerTest extends ApiTestSupport { + + private static final int CHAT_REWARD = 2000; + private static final String INQUIRY_MESSAGE = "와"; + + @Autowired + private ChatMessageRepository chatMessageRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private QuestionPostRepository questionPostRepository; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @Autowired + private ChatInquiryRepository chatInquiryRepository; + + @Autowired + private CreditHistoryRepository creditHistoryRepository; + + @AfterEach + void teardown() { + creditHistoryRepository.deleteAll(); + memberRepository.deleteAll(); + questionPostRepository.deleteAll(); + chatRoomRepository.deleteAll(); + chatMessageRepository.deleteAll(); + } + + @DisplayName("[답변자 아이디로 채팅을 요청할 수 있다.]") + @Test + void createChatInquiry() throws Exception { + //given + Member answerer = memberRepository.save(MemberFixture.member5()); + QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember)); + CreateChatInquiryRequest request = new CreateChatInquiryRequest( + questionPost.getId(), + answerer.getId(), + INQUIRY_MESSAGE + ); + //when & then + mockMvc.perform(post("/api/chat/inquiries") + .cookie(accessToken) + .content(toJson(request)) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.inquiryStatus").value(InquiryStatus.PENDING.getLabel())) // 내림차순 + .andDo(MockMvcResultHandlers.print()); + } + + @DisplayName("[회원의 채팅 요청 목록을 조회할 수 있다.]") + @Test + void getChatInquiresByMember() throws Exception { + //given + Member member1 = memberRepository.save(MemberFixture.member4()); + Member member2 = memberRepository.save(MemberFixture.member5()); + List questionPosts = questionPostRepository.saveAll( + List.of( + questionPostRepository.save(QuestionPostFixture.questionPost(member1)), + questionPostRepository.save(QuestionPostFixture.questionPost(member2)) + ) + ); + ChatInquiry chatInquiry1 = chatInquiryRepository.save( + ChatInquiryFixture.chatInquiry(questionPosts.get(0), member1, loginMember, INQUIRY_MESSAGE)); + ChatInquiry chatInquiry2 = chatInquiryRepository.save( + ChatInquiryFixture.chatInquiry(questionPosts.get(1), loginMember, member2, INQUIRY_MESSAGE)); + + // when & then + mockMvc.perform(get("/api/chat/inquiries") + .cookie(accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].chatInquiryId").value(chatInquiry2.getId())) // 내림차순 + .andExpect(jsonPath("$.content[0].partnerInfo.memberId").value(member2.getId())) + .andExpect(jsonPath("$.content[0].isInquirer").value(true)) + .andExpect(jsonPath("$.content[0].inquiryStatus").value(InquiryStatus.PENDING.getLabel())) + + .andExpect(jsonPath("$.content[1].chatInquiryId").value(chatInquiry1.getId())) + .andExpect(jsonPath("$.content[1].partnerInfo.memberId").value(member1.getId())) + .andExpect(jsonPath("$.content[1].isInquirer").value(false)) + .andExpect(jsonPath("$.content[1].inquiryStatus").value(InquiryStatus.PENDING.getLabel())) + .andDo(MockMvcResultHandlers.print()); + } + + @DisplayName("[답변자가 채팅 요청을 수락할 수 있다.]") + @Test + void acceptChatRoom() throws Exception { + Member inquirer = memberRepository.save(MemberFixture.member4()); + QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(inquirer)); + ChatInquiry chatInquiry = chatInquiryRepository.save( + ChatInquiryFixture.chatInquiry(questionPost, inquirer, loginMember, INQUIRY_MESSAGE)); + int previousAnswererCredit = loginMember.getCredit(); + + mockMvc.perform(patch("/api/chat/inquiries/{chatInquiryId}/accept", chatInquiry.getId()) + .cookie(accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.inquiryStatus").value(InquiryStatus.ACCEPTED.getLabel())) + .andExpect(jsonPath("$.credit").value(previousAnswererCredit + CHAT_REWARD)); + } + + @DisplayName("[답변자가 채팅 요청을 거절할 수 있다.]") + @Test + void rejectChatRoom() throws Exception { + Member inquirer = memberRepository.save(MemberFixture.member4()); + QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(inquirer)); + ChatInquiry chatInquiry = chatInquiryRepository.save( + ChatInquiryFixture.chatInquiry(questionPost, inquirer, loginMember, INQUIRY_MESSAGE)); + + mockMvc.perform(patch("/api/chat/inquiries/{chatInquiryId}/reject", chatInquiry.getId()) + .cookie(accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.inquiryStatus").value(InquiryStatus.REJECTED.getLabel())); + } +} \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepositoryTest.java b/src/test/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepositoryTest.java new file mode 100644 index 00000000..79ef7307 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/chat_inquiry/repository/ChatInquiryRepositoryTest.java @@ -0,0 +1,101 @@ +package com.dnd.gongmuin.chat_inquiry.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.util.ReflectionTestUtils; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; +import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryResponse; +import com.dnd.gongmuin.common.fixture.ChatInquiryFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.common.support.DataJpaTestSupport; +import com.dnd.gongmuin.credit_history.repository.CreditHistoryRepository; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.repository.MemberRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +@DisplayName("[ChatInquiryRepository 테스트]") +class ChatInquiryRepositoryTest extends DataJpaTestSupport { + + private final PageRequest pageRequest = PageRequest.of(0, 10); + private final String chatMessage = "와우"; + + @Autowired + ChatInquiryRepository chatInquiryRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + QuestionPostRepository questionPostRepository; + + @Autowired + CreditHistoryRepository creditHistoryRepository; + + @DisplayName("회원의 채팅 요청 목록을 조회할 수 있다.") + @Test + void getChatInquiresByMember() { + //given + Member questioner = memberRepository.save(MemberFixture.member()); + Member target = memberRepository.save(MemberFixture.member()); + Member answerer = memberRepository.save(MemberFixture.member()); + QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(questioner)); + List chatInquiries = chatInquiryRepository.saveAll(List.of( + chatInquiryRepository.save(ChatInquiryFixture.chatInquiry(questionPost, target, answerer, chatMessage)), + chatInquiryRepository.save(ChatInquiryFixture.chatInquiry(questionPost, questioner, target, chatMessage)), + chatInquiryRepository.save(ChatInquiryFixture.chatInquiry(questionPost, questioner, answerer, chatMessage)) + )); + + //when + List responses = chatInquiryRepository.getChatInquiresByMember(target, pageRequest) + .getContent(); + + //then + Assertions.assertAll( + () -> assertThat(responses).hasSize(2), + () -> assertThat(responses.get(0).chatInquiryId()).isEqualTo(chatInquiries.get(1).getId()), + () -> assertThat(responses.get(0).partnerInfo().memberId()).isEqualTo(questioner.getId()), + () -> assertThat(responses.get(1).chatInquiryId()).isEqualTo(chatInquiries.get(0).getId()), + () -> assertThat(responses.get(1).partnerInfo().memberId()).isEqualTo(answerer.getId()) + ); + } + + @DisplayName("요청중인 채팅방이 일주일이 지나면, 채팅방 상태를 거절함으로 바꾼다.") + @Test + void updateChatInquiryStatusRejected() { + //given + Member questioner = memberRepository.save(MemberFixture.member()); + Member answerer = memberRepository.save(MemberFixture.member()); + QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(questioner)); + + List chatInquiries = chatInquiryRepository.saveAll(List.of( + chatInquiryRepository.save(ChatInquiryFixture.chatInquiry(questionPost, questioner, answerer, chatMessage)), + chatInquiryRepository.save(ChatInquiryFixture.chatInquiry(questionPost, questioner, answerer, chatMessage)) + )); + ReflectionTestUtils.setField(chatInquiries.get(0), "createdAt", LocalDateTime.now().minusWeeks(1)); + + //when + chatInquiryRepository.updateChatInquiryStatusRejected(); + + em.flush(); + em.clear(); + + //then + ChatInquiry chatInquiry1 = chatInquiryRepository.findById(chatInquiries.get(0).getId()).orElseThrow(); + ChatInquiry chatInquiry2 = chatInquiryRepository.findById(chatInquiries.get(1).getId()).orElseThrow(); + assertAll( + () -> assertThat(chatInquiry1.getStatus()).isEqualTo(InquiryStatus.REJECTED), + () -> assertThat(chatInquiry2.getStatus()).isEqualTo(InquiryStatus.PENDING) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryServiceTest.java b/src/test/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryServiceTest.java new file mode 100644 index 00000000..b12c9e26 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/chat_inquiry/service/ChatInquiryServiceTest.java @@ -0,0 +1,283 @@ +package com.dnd.gongmuin.chat_inquiry.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; + +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.data.domain.PageRequest; +import org.springframework.data.domain.SliceImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; +import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; +import com.dnd.gongmuin.chat_inquiry.dto.AcceptChatResponse; +import com.dnd.gongmuin.chat_inquiry.dto.ChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryRequest; +import com.dnd.gongmuin.chat_inquiry.dto.CreateChatInquiryResponse; +import com.dnd.gongmuin.chat_inquiry.dto.RejectChatResponse; +import com.dnd.gongmuin.chat_inquiry.repository.ChatInquiryRepository; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; +import com.dnd.gongmuin.chatroom.repository.ChatRoomRepository; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; +import com.dnd.gongmuin.common.fixture.ChatInquiryFixture; +import com.dnd.gongmuin.common.fixture.ChatRoomFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.credit_history.domain.CreditType; +import com.dnd.gongmuin.credit_history.service.CreditHistoryService; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.exception.MemberErrorCode; +import com.dnd.gongmuin.member.repository.MemberRepository; +import com.dnd.gongmuin.notification.dto.NotificationEvent; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +@DisplayName("[채팅 요청 서비스 단위 테스트]") +@ExtendWith(MockitoExtension.class) +class ChatInquiryServiceTest { + + private static final int CHAT_REWARD = 2000; + private static final String INQUIRY_MESSAGE = "채팅을 요청합니다."; + private final PageRequest pageRequest = PageRequest.of(0, 5); + + @Mock + private ChatRoomRepository chatRoomRepository; + + @Mock + private ChatInquiryRepository chatInquiryRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private QuestionPostRepository questionPostRepository; + + @Mock + private ApplicationEventPublisher eventPublisher; + + @Mock + private CreditHistoryService creditHistoryService; + + @InjectMocks + private ChatInquiryService chatInquiryService; + + @DisplayName("[댓글 작성자에게 채팅을 요청할 수 있다.]") + @Test + void createInquiry() { + //given + Long chatInquiryId = 1L; + Member inquirer = MemberFixture.member(1L); + Member answerer = MemberFixture.member(2L); + QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); + ChatInquiry chatInquiry = ChatInquiryFixture.chatInquiry( + chatInquiryId, questionPost, inquirer, answerer, INQUIRY_MESSAGE + ); + CreateChatInquiryRequest request = new CreateChatInquiryRequest( + questionPost.getId(), + answerer.getId(), + INQUIRY_MESSAGE + ); + + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + given(memberRepository.findById(answerer.getId())) + .willReturn(Optional.of(answerer)); + given(chatInquiryRepository.save(any(ChatInquiry.class))).willReturn(chatInquiry); + + CreateChatInquiryResponse response = chatInquiryService.createChatInquiry(request, inquirer); + assertAll( + () -> assertThat(response.chatInquiryId()) + .isEqualTo(chatInquiryId), + () -> assertThat(response.partnerInfo().memberId()) + .isEqualTo(answerer.getId()), + () -> assertThat(response.inquiryMessage()) + .isEqualTo(INQUIRY_MESSAGE) + ); + } + + @DisplayName("[요청자의 크레딧이 2000미만이면 채팅을 요청할 수 없다.]") + @Test + void createInquiry_fails() { + //given + Member inquirer = MemberFixture.member(1L); + Member answerer = MemberFixture.member(2L); + ReflectionTestUtils.setField(inquirer, "credit", CHAT_REWARD - 1); + QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); + CreateChatInquiryRequest request = new CreateChatInquiryRequest( + questionPost.getId(), + answerer.getId(), + INQUIRY_MESSAGE + ); + + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + given(memberRepository.findById(answerer.getId())) + .willReturn(Optional.of(answerer)); + + //when & then + assertThatThrownBy(() -> chatInquiryService.createChatInquiry(request, inquirer)) + .isInstanceOf(ValidationException.class) + .hasMessageContaining(MemberErrorCode.NOT_ENOUGH_CREDIT.getMessage()); + } + + @DisplayName("[회원이 속한 채팅 요청 목록을 조회할 수 있다.]") + @Test + void getChatInquiresByMember() { + //given + Long chatInquiryId = 1L; + Member targetMember = MemberFixture.member(1L); + Member partner = MemberFixture.member(2L); + ChatInquiryResponse chatInquiryResponse = new ChatInquiryResponse( + chatInquiryId, INQUIRY_MESSAGE, InquiryStatus.PENDING, true, partner.getId(), + partner.getNickname(), partner.getJobGroup(), partner.getProfileImageNo() + ); + given(chatInquiryRepository.getChatInquiresByMember(targetMember, pageRequest)) + .willReturn(new SliceImpl<>(List.of(chatInquiryResponse), pageRequest, false)); + + //when + List response = chatInquiryService.getChatInquiresByMember( + targetMember, pageRequest).content(); + + //then + assertAll( + () -> assertThat(response).hasSize(1), + () -> assertThat(response.get(0).chatInquiryId()) + .isEqualTo(chatInquiryId), + () -> assertThat(response.get(0).partnerInfo().memberId()) + .isEqualTo(partner.getId()), + () -> assertThat(response.get(0).message()) + .isEqualTo(INQUIRY_MESSAGE) + ); + } + + @DisplayName("[답변자가 채팅 요청을 수락할 수 있다.]") + @Test + void acceptChat() { + //given + Long chatInquiryId = 1L; + Member inquirer = MemberFixture.member(1L); + Member answerer = MemberFixture.member(2L); + int previousCredit = answerer.getCredit(); + QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); + ChatInquiry chatInquiry = ChatInquiryFixture.chatInquiry(questionPost, inquirer, answerer, INQUIRY_MESSAGE); + ChatRoom chatRoom = ChatRoomFixture.chatRoom(1L, questionPost, inquirer, answerer); + given(chatInquiryRepository.findById(chatInquiryId)) + .willReturn(Optional.of(chatInquiry)); + given(chatRoomRepository.save(any(ChatRoom.class))) + .willReturn(chatRoom); + + //when + AcceptChatResponse response = chatInquiryService.acceptChat(chatInquiryId, answerer); + + //then + assertAll( + () -> assertThat(response.inquiryStatus()) + .isEqualTo(InquiryStatus.ACCEPTED.getLabel()), + () -> assertThat(response.credit()) + .isEqualTo(previousCredit + CHAT_REWARD) + ); + } + + @DisplayName("[답변자가 채팅 요청을 수락할 때 채팅 수락 알림이 발행된다.]") + @Test + void acceptChatWithEventPublish() { + //given + Long chatInquiryId = 1L; + Member inquirer = MemberFixture.member(1L); + Member answerer = MemberFixture.member(2L); + int previousCredit = answerer.getCredit(); + QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); + ChatInquiry chatInquiry = ChatInquiryFixture.chatInquiry(questionPost, inquirer, answerer, INQUIRY_MESSAGE); + ChatRoom chatRoom = ChatRoomFixture.chatRoom(1L, questionPost, inquirer, answerer); + given(chatInquiryRepository.findById(chatInquiryId)) + .willReturn(Optional.of(chatInquiry)); + given(chatRoomRepository.save(any(ChatRoom.class))) + .willReturn(chatRoom); + + //when + AcceptChatResponse response = chatInquiryService.acceptChat(chatInquiryId, answerer); + + //then + assertAll( + () -> assertThat(response.inquiryStatus()) + .isEqualTo(InquiryStatus.ACCEPTED.getLabel()), + () -> assertThat(response.credit()) + .isEqualTo(previousCredit + CHAT_REWARD), + () -> verify(eventPublisher, times(1)).publishEvent(any(NotificationEvent.class)) + ); + } + + @DisplayName("[답변자가 채팅 요청을 거절할 수 있다.]") + @Test + void rejectChat() { + //given + Long chatInquiryId = 1L; + Member inquirer = MemberFixture.member(1L); + Member answerer = MemberFixture.member(2L); + QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); + ChatInquiry chatInquiry = ChatInquiryFixture.chatInquiry(questionPost, inquirer, answerer, INQUIRY_MESSAGE); + + given(chatInquiryRepository.findById(chatInquiryId)) + .willReturn(Optional.of(chatInquiry)); + + //when + RejectChatResponse response = chatInquiryService.rejectChat(chatInquiryId, answerer); + + //then + assertThat(response.inquiryStatus()) + .isEqualTo(InquiryStatus.REJECTED.getLabel()); + } + + @DisplayName("[답변자가 채팅 요청을 거절할 때 채팅 거절 알림이 발행된다.]") + @Test + void rejectChatWithEventPublish() { + //given + Long chatInquiryId = 1L; + Member inquirer = MemberFixture.member(1L); + Member answerer = MemberFixture.member(2L); + QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer); + ChatInquiry chatInquiry = ChatInquiryFixture.chatInquiry(questionPost, inquirer, answerer, INQUIRY_MESSAGE); + + given(chatInquiryRepository.findById(chatInquiryId)) + .willReturn(Optional.of(chatInquiry)); + + //when + RejectChatResponse response = chatInquiryService.rejectChat(chatInquiryId, answerer); + + //then + assertAll( + () -> assertThat(response.inquiryStatus()).isEqualTo(InquiryStatus.REJECTED.getLabel()), + () -> verify(eventPublisher, times(1)).publishEvent(any(NotificationEvent.class)) + ); + } + + @DisplayName("일주일이 지난 요청에 경우 자동으로 거절하고, 요청자에게 크레딧을 반환한다.") + @Test + void rejectChatAuto() { + // given + List rejectedInquirerIds = List.of(1L, 2L); + given(chatInquiryRepository.getAutoRejectedInquirerIds()) + .willReturn(rejectedInquirerIds); + + // when + chatInquiryService.rejectChatAuto(); + + // then + verify(chatInquiryRepository).getAutoRejectedInquirerIds(); + verify(chatInquiryRepository).updateChatInquiryStatusRejected(); + verify(memberRepository).refundInMemberIds(rejectedInquirerIds, CHAT_REWARD); + verify(creditHistoryService).saveCreditHistoryInMemberIds( + rejectedInquirerIds, CreditType.CHAT_REFUND, CHAT_REWARD + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/ChatInquiryFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/ChatInquiryFixture.java new file mode 100644 index 00000000..4cd9a170 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/common/fixture/ChatInquiryFixture.java @@ -0,0 +1,45 @@ +package com.dnd.gongmuin.common.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.question_post.domain.QuestionPost; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ChatInquiryFixture { + + public static ChatInquiry chatInquiry( + QuestionPost questionPost, + Member inquirer, + Member answerer, + String message + ) { + return ChatInquiry.of( + questionPost, + inquirer, + answerer, + message + ); + } + + public static ChatInquiry chatInquiry( + Long id, + QuestionPost questionPost, + Member inquirer, + Member answerer, + String message + ) { + ChatInquiry chatInquiry = ChatInquiry.of( + questionPost, + inquirer, + answerer, + message + ); + ReflectionTestUtils.setField(chatInquiry, "id", id); + return chatInquiry; + } +} diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java index f907d048..bc859239 100644 --- a/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java +++ b/src/test/java/com/dnd/gongmuin/common/fixture/ChatMessageFixture.java @@ -4,8 +4,8 @@ import org.springframework.test.util.ReflectionTestUtils; -import com.dnd.gongmuin.chat.domain.ChatMessage; -import com.dnd.gongmuin.chat.domain.MessageType; +import com.dnd.gongmuin.chatroom.domain.ChatMessage; +import com.dnd.gongmuin.chatroom.domain.MessageType; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/ChatRoomFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/ChatRoomFixture.java index e0406b68..0a73c27f 100644 --- a/src/test/java/com/dnd/gongmuin/common/fixture/ChatRoomFixture.java +++ b/src/test/java/com/dnd/gongmuin/common/fixture/ChatRoomFixture.java @@ -2,8 +2,7 @@ import org.springframework.test.util.ReflectionTestUtils; -import com.dnd.gongmuin.chat.domain.ChatRoom; -import com.dnd.gongmuin.chat.domain.ChatStatus; +import com.dnd.gongmuin.chatroom.domain.ChatRoom; import com.dnd.gongmuin.member.domain.Member; import com.dnd.gongmuin.question_post.domain.QuestionPost; @@ -25,20 +24,6 @@ public static ChatRoom chatRoom( ); } - public static ChatRoom acceptedChatRoom( - QuestionPost questionPost, - Member inquirer, - Member answerer - ) { - ChatRoom chatRoom = ChatRoom.of( - questionPost, - inquirer, - answerer - ); - ReflectionTestUtils.setField(chatRoom, "status", ChatStatus.ACCEPTED); - return chatRoom; - } - public static ChatRoom chatRoom( Long id, QuestionPost questionPost,