diff --git a/build.gradle b/build.gradle index 2de93e064..91cc2e77d 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,7 @@ dependencies { testImplementation 'org.testcontainers:mysql' testImplementation 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.awaitility:awaitility:4.2.0' // Etc implementation 'org.hibernate.validator:hibernate-validator' diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java index e8e7a3ebb..fc159c2cd 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java @@ -2,6 +2,7 @@ import com.example.solidconnection.common.BaseEntity; import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -25,6 +26,9 @@ public class ChatRoom extends BaseEntity { private boolean isGroup = false; + @Column(name = "mentoring_id", unique = true) + private Long mentoringId; + @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL) @BatchSize(size = 10) private final List chatParticipants = new ArrayList<>(); @@ -35,4 +39,9 @@ public class ChatRoom extends BaseEntity { public ChatRoom(boolean isGroup) { this.isGroup = isGroup; } + + public ChatRoom(Long mentoringId, boolean isGroup) { + this.mentoringId = mentoringId; + this.isGroup = isGroup; + } } diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java index dd5193abf..ad815dbe1 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java @@ -33,4 +33,6 @@ SELECT COUNT(cm) FROM ChatMessage cm AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt) """) long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId); + + boolean existsByMentoringId(long mentoringId); } diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index fadd284fe..4a9c02eed 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -162,4 +162,17 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } + + @Transactional + public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) { + if (chatRoomRepository.existsByMentoringId(mentoringId)) { + return; + } + + ChatRoom chatRoom = new ChatRoom(mentoringId, false); + chatRoom = chatRoomRepository.save(chatRoom); + ChatParticipant mentorParticipant = new ChatParticipant(mentorId, chatRoom); + ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom); + chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant)); + } } diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringApprovedEvent.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringApprovedEvent.java new file mode 100644 index 000000000..4909a5c85 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringApprovedEvent.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.mentor.dto; + +public record MentoringApprovedEvent( + long mentoringId, + long mentorId, + long menteeId +) { + + public static MentoringApprovedEvent of(long mentoringId, long mentorId, long menteeId) { + return new MentoringApprovedEvent(mentoringId, mentorId, menteeId); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java index dd4e98936..15a1e2507 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java @@ -11,11 +11,13 @@ import com.example.solidconnection.mentor.domain.Mentoring; import com.example.solidconnection.mentor.dto.MentoringApplyRequest; import com.example.solidconnection.mentor.dto.MentoringApplyResponse; +import com.example.solidconnection.mentor.dto.MentoringApprovedEvent; import com.example.solidconnection.mentor.dto.MentoringConfirmRequest; import com.example.solidconnection.mentor.dto.MentoringConfirmResponse; import com.example.solidconnection.mentor.repository.MentorRepository; import com.example.solidconnection.mentor.repository.MentoringRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +27,7 @@ public class MentoringCommandService { private final MentoringRepository mentoringRepository; private final MentorRepository mentorRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) { @@ -48,6 +51,7 @@ public MentoringConfirmResponse confirmMentoring(long siteUserId, long mentoring if (mentoringConfirmRequest.status() == VerifyStatus.APPROVED) { mentor.increaseMenteeCount(); + eventPublisher.publishEvent(MentoringApprovedEvent.of(mentoringId, mentor.getSiteUserId(), mentoring.getMenteeId())); } return MentoringConfirmResponse.from(mentoring); diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringEventHandler.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringEventHandler.java new file mode 100644 index 000000000..920ff007f --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringEventHandler.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.chat.service.ChatService; +import com.example.solidconnection.mentor.dto.MentoringApprovedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class MentoringEventHandler { + + private final ChatService chatService; + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener + public void handleMentoringApproved(MentoringApprovedEvent event) { + chatService.createMentoringChatRoom(event.mentoringId(), event.mentorId(), event.menteeId()); + } +} diff --git a/src/main/resources/db/migration/V28__add_mentoring_id_to_chat_room.sql b/src/main/resources/db/migration/V28__add_mentoring_id_to_chat_room.sql new file mode 100644 index 000000000..f4ab8d815 --- /dev/null +++ b/src/main/resources/db/migration/V28__add_mentoring_id_to_chat_room.sql @@ -0,0 +1,5 @@ +ALTER TABLE chat_room + ADD COLUMN mentoring_id BIGINT, +ADD CONSTRAINT uk_chat_room_mentoring_id UNIQUE (mentoring_id), +ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id); + diff --git a/src/test/java/com/example/solidconnection/chat/repository/ChatRoomRepositoryForTest.java b/src/test/java/com/example/solidconnection/chat/repository/ChatRoomRepositoryForTest.java new file mode 100644 index 000000000..7605453c6 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/repository/ChatRoomRepositoryForTest.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatRoom; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ChatRoomRepositoryForTest extends JpaRepository { + + @Query(""" + SELECT DISTINCT cr FROM ChatRoom cr + LEFT JOIN FETCH cr.chatParticipants cp + WHERE cr.isGroup = false + AND EXISTS ( + SELECT 1 FROM ChatParticipant cp1 + WHERE cp1.chatRoom = cr AND cp1.siteUserId = :mentorId + ) + AND EXISTS ( + SELECT 1 FROM ChatParticipant cp2 + WHERE cp2.chatRoom = cr AND cp2.siteUserId = :menteeId + ) + AND ( + SELECT COUNT(cp3) FROM ChatParticipant cp3 + WHERE cp3.chatRoom = cr + ) = 2 + """) + Optional findOneOnOneChatRoomByParticipants(@Param("mentorId") long mentorId, @Param("menteeId") long menteeId); +} diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java index a92925653..058388d51 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java @@ -6,7 +6,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; - +import static org.awaitility.Awaitility.await; +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.repository.ChatRoomRepositoryForTest; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Mentor; @@ -22,6 +25,9 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.time.Duration; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -41,6 +47,9 @@ class MentoringCommandServiceTest { @Autowired private MentoringRepository mentoringRepository; + @Autowired + private ChatRoomRepositoryForTest chatRoomRepositoryForTest; + @Autowired private SiteUserFixture siteUserFixture; @@ -115,6 +124,36 @@ class 멘토링_승인_거절_테스트 { ); } + @Test + void 멘토링_승인시_채팅방이_자동으로_생성된다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED); + + Optional beforeChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); + assertThat(beforeChatRoom).isEmpty(); + + // when + mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); + + // then + ChatRoom afterChatRoom = await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(100)) + .until(() -> chatRoomRepositoryForTest + .findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()), + Optional::isPresent) + .orElseThrow(); + + List participantIds = afterChatRoom.getChatParticipants().stream() + .map(ChatParticipant::getSiteUserId) + .toList(); + assertAll( + () -> assertThat(afterChatRoom.isGroup()).isFalse(), + () -> assertThat(participantIds).containsExactly(mentorUser1.getId(), menteeUser.getId()) + ); + } + @Test void 멘토링을_성공적으로_거절한다() { // given @@ -137,6 +176,31 @@ class 멘토링_승인_거절_테스트 { ); } + @Test + void 멘토링_거절시_채팅방이_자동으로_생성되지_않는다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED); + + Optional beforeChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); + assertThat(beforeChatRoom).isEmpty(); + + // when + mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); + + // then + await() + .pollInterval(Duration.ofMillis(100)) + .during(Duration.ofSeconds(1)) + .until(() -> chatRoomRepositoryForTest + .findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()) + .isEmpty()); + + Optional afterChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); + assertThat(afterChatRoom).isEmpty(); + + } + @Test void 다른_멘토의_멘토링을_승인할_수_없다() { // given