Skip to content

Commit 655e0bd

Browse files
authored
feat: 채팅방 생성 기능 추가 (#439)
* feat: mentoring_id 컬럼 추가 및 유니크 제약조건 추가 - 멘토링의 경우에만 mentoringId가 존재하므로 long이 아닌 Long으로 설정 * feat: 멘토링 채팅방 생성 서비스 로직 추가 * feat: 멘토링 승인 시 이벤트 기반 호출 기능 추가 * test: 채팅방 개설 관련 테스트 코드 작성 * refactor: @TransactionalEventListener로 변경 * fix: 잘못된 어노테이션 import 변경 * refactor: DISTINCT를 추가하여 데이터 중복 해결 * refactor: @column에 유니크키 설정하도록 변경 * refactor: fk 추가 * refactor: find -> exists로 변경하여 성능 개선 * test: 비동기 이벤트 처리 완료까지 대기 및 재시도하도록 수정 * test: awaitility:4.2.0 도입
1 parent 289cd89 commit 655e0bd

File tree

10 files changed

+164
-1
lines changed

10 files changed

+164
-1
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ dependencies {
6262
testImplementation 'org.testcontainers:mysql'
6363
testImplementation 'org.projectlombok:lombok'
6464
testAnnotationProcessor 'org.projectlombok:lombok'
65+
testImplementation 'org.awaitility:awaitility:4.2.0'
6566

6667
// Etc
6768
implementation 'org.hibernate.validator:hibernate-validator'

src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.example.solidconnection.common.BaseEntity;
44
import jakarta.persistence.CascadeType;
5+
import jakarta.persistence.Column;
56
import jakarta.persistence.Entity;
67
import jakarta.persistence.GeneratedValue;
78
import jakarta.persistence.GenerationType;
@@ -25,6 +26,9 @@ public class ChatRoom extends BaseEntity {
2526

2627
private boolean isGroup = false;
2728

29+
@Column(name = "mentoring_id", unique = true)
30+
private Long mentoringId;
31+
2832
@OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL)
2933
@BatchSize(size = 10)
3034
private final List<ChatParticipant> chatParticipants = new ArrayList<>();
@@ -35,4 +39,9 @@ public class ChatRoom extends BaseEntity {
3539
public ChatRoom(boolean isGroup) {
3640
this.isGroup = isGroup;
3741
}
42+
43+
public ChatRoom(Long mentoringId, boolean isGroup) {
44+
this.mentoringId = mentoringId;
45+
this.isGroup = isGroup;
46+
}
3847
}

src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ SELECT COUNT(cm) FROM ChatMessage cm
3333
AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt)
3434
""")
3535
long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId);
36+
37+
boolean existsByMentoringId(long mentoringId);
3638
}

src/main/java/com/example/solidconnection/chat/service/ChatService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,17 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long
162162

163163
simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
164164
}
165+
166+
@Transactional
167+
public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) {
168+
if (chatRoomRepository.existsByMentoringId(mentoringId)) {
169+
return;
170+
}
171+
172+
ChatRoom chatRoom = new ChatRoom(mentoringId, false);
173+
chatRoom = chatRoomRepository.save(chatRoom);
174+
ChatParticipant mentorParticipant = new ChatParticipant(mentorId, chatRoom);
175+
ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom);
176+
chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant));
177+
}
165178
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.example.solidconnection.mentor.dto;
2+
3+
public record MentoringApprovedEvent(
4+
long mentoringId,
5+
long mentorId,
6+
long menteeId
7+
) {
8+
9+
public static MentoringApprovedEvent of(long mentoringId, long mentorId, long menteeId) {
10+
return new MentoringApprovedEvent(mentoringId, mentorId, menteeId);
11+
}
12+
}

src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
import com.example.solidconnection.mentor.domain.Mentoring;
1212
import com.example.solidconnection.mentor.dto.MentoringApplyRequest;
1313
import com.example.solidconnection.mentor.dto.MentoringApplyResponse;
14+
import com.example.solidconnection.mentor.dto.MentoringApprovedEvent;
1415
import com.example.solidconnection.mentor.dto.MentoringConfirmRequest;
1516
import com.example.solidconnection.mentor.dto.MentoringConfirmResponse;
1617
import com.example.solidconnection.mentor.repository.MentorRepository;
1718
import com.example.solidconnection.mentor.repository.MentoringRepository;
1819
import lombok.RequiredArgsConstructor;
20+
import org.springframework.context.ApplicationEventPublisher;
1921
import org.springframework.stereotype.Service;
2022
import org.springframework.transaction.annotation.Transactional;
2123

@@ -25,6 +27,7 @@ public class MentoringCommandService {
2527

2628
private final MentoringRepository mentoringRepository;
2729
private final MentorRepository mentorRepository;
30+
private final ApplicationEventPublisher eventPublisher;
2831

2932
@Transactional
3033
public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) {
@@ -48,6 +51,7 @@ public MentoringConfirmResponse confirmMentoring(long siteUserId, long mentoring
4851

4952
if (mentoringConfirmRequest.status() == VerifyStatus.APPROVED) {
5053
mentor.increaseMenteeCount();
54+
eventPublisher.publishEvent(MentoringApprovedEvent.of(mentoringId, mentor.getSiteUserId(), mentoring.getMenteeId()));
5155
}
5256

5357
return MentoringConfirmResponse.from(mentoring);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example.solidconnection.mentor.service;
2+
3+
import com.example.solidconnection.chat.service.ChatService;
4+
import com.example.solidconnection.mentor.dto.MentoringApprovedEvent;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.scheduling.annotation.Async;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.transaction.annotation.Propagation;
9+
import org.springframework.transaction.annotation.Transactional;
10+
import org.springframework.transaction.event.TransactionalEventListener;
11+
12+
@Component
13+
@RequiredArgsConstructor
14+
public class MentoringEventHandler {
15+
16+
private final ChatService chatService;
17+
18+
@Async
19+
@Transactional(propagation = Propagation.REQUIRES_NEW)
20+
@TransactionalEventListener
21+
public void handleMentoringApproved(MentoringApprovedEvent event) {
22+
chatService.createMentoringChatRoom(event.mentoringId(), event.mentorId(), event.menteeId());
23+
}
24+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ALTER TABLE chat_room
2+
ADD COLUMN mentoring_id BIGINT,
3+
ADD CONSTRAINT uk_chat_room_mentoring_id UNIQUE (mentoring_id),
4+
ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
5+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.example.solidconnection.chat.repository;
2+
3+
import com.example.solidconnection.chat.domain.ChatRoom;
4+
import java.util.Optional;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
9+
public interface ChatRoomRepositoryForTest extends JpaRepository<ChatRoom, Long> {
10+
11+
@Query("""
12+
SELECT DISTINCT cr FROM ChatRoom cr
13+
LEFT JOIN FETCH cr.chatParticipants cp
14+
WHERE cr.isGroup = false
15+
AND EXISTS (
16+
SELECT 1 FROM ChatParticipant cp1
17+
WHERE cp1.chatRoom = cr AND cp1.siteUserId = :mentorId
18+
)
19+
AND EXISTS (
20+
SELECT 1 FROM ChatParticipant cp2
21+
WHERE cp2.chatRoom = cr AND cp2.siteUserId = :menteeId
22+
)
23+
AND (
24+
SELECT COUNT(cp3) FROM ChatParticipant cp3
25+
WHERE cp3.chatRoom = cr
26+
) = 2
27+
""")
28+
Optional<ChatRoom> findOneOnOneChatRoomByParticipants(@Param("mentorId") long mentorId, @Param("menteeId") long menteeId);
29+
}

src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import static org.assertj.core.api.Assertions.assertThat;
77
import static org.assertj.core.api.Assertions.assertThatThrownBy;
88
import static org.junit.jupiter.api.Assertions.assertAll;
9-
9+
import static org.awaitility.Awaitility.await;
10+
import com.example.solidconnection.chat.domain.ChatParticipant;
11+
import com.example.solidconnection.chat.domain.ChatRoom;
12+
import com.example.solidconnection.chat.repository.ChatRoomRepositoryForTest;
1013
import com.example.solidconnection.common.VerifyStatus;
1114
import com.example.solidconnection.common.exception.CustomException;
1215
import com.example.solidconnection.mentor.domain.Mentor;
@@ -22,6 +25,9 @@
2225
import com.example.solidconnection.siteuser.domain.SiteUser;
2326
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
2427
import com.example.solidconnection.support.TestContainerSpringBootTest;
28+
import java.time.Duration;
29+
import java.util.List;
30+
import java.util.Optional;
2531
import org.junit.jupiter.api.BeforeEach;
2632
import org.junit.jupiter.api.DisplayName;
2733
import org.junit.jupiter.api.Nested;
@@ -41,6 +47,9 @@ class MentoringCommandServiceTest {
4147
@Autowired
4248
private MentoringRepository mentoringRepository;
4349

50+
@Autowired
51+
private ChatRoomRepositoryForTest chatRoomRepositoryForTest;
52+
4453
@Autowired
4554
private SiteUserFixture siteUserFixture;
4655

@@ -115,6 +124,36 @@ class 멘토링_승인_거절_테스트 {
115124
);
116125
}
117126

127+
@Test
128+
void 멘토링_승인시_채팅방이_자동으로_생성된다() {
129+
// given
130+
Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId());
131+
MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED);
132+
133+
Optional<ChatRoom> beforeChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId());
134+
assertThat(beforeChatRoom).isEmpty();
135+
136+
// when
137+
mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request);
138+
139+
// then
140+
ChatRoom afterChatRoom = await()
141+
.atMost(Duration.ofSeconds(5))
142+
.pollInterval(Duration.ofMillis(100))
143+
.until(() -> chatRoomRepositoryForTest
144+
.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()),
145+
Optional::isPresent)
146+
.orElseThrow();
147+
148+
List<Long> participantIds = afterChatRoom.getChatParticipants().stream()
149+
.map(ChatParticipant::getSiteUserId)
150+
.toList();
151+
assertAll(
152+
() -> assertThat(afterChatRoom.isGroup()).isFalse(),
153+
() -> assertThat(participantIds).containsExactly(mentorUser1.getId(), menteeUser.getId())
154+
);
155+
}
156+
118157
@Test
119158
void 멘토링을_성공적으로_거절한다() {
120159
// given
@@ -137,6 +176,31 @@ class 멘토링_승인_거절_테스트 {
137176
);
138177
}
139178

179+
@Test
180+
void 멘토링_거절시_채팅방이_자동으로_생성되지_않는다() {
181+
// given
182+
Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId());
183+
MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED);
184+
185+
Optional<ChatRoom> beforeChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId());
186+
assertThat(beforeChatRoom).isEmpty();
187+
188+
// when
189+
mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request);
190+
191+
// then
192+
await()
193+
.pollInterval(Duration.ofMillis(100))
194+
.during(Duration.ofSeconds(1))
195+
.until(() -> chatRoomRepositoryForTest
196+
.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId())
197+
.isEmpty());
198+
199+
Optional<ChatRoom> afterChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId());
200+
assertThat(afterChatRoom).isEmpty();
201+
202+
}
203+
140204
@Test
141205
void 다른_멘토의_멘토링을_승인할_수_없다() {
142206
// given

0 commit comments

Comments
 (0)