Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.sofa.linkiving.domain.chat.ai.TitleClient;
Expand Down Expand Up @@ -66,34 +65,25 @@ public void deleteChat(Member member, Long chatId) {
chatService.delete(chat);
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional
public void generateAnswer(Long chatId, Member member, String message) {
try {
chatService.getChat(chatId, member);
} catch (RuntimeException ex) {
log.error("채팅 답변 시작 중 오류 발생 - chatId: {}, error: {}", chatId, ex.getMessage(), ex);
sendNotification(chatId, member, AnswerRes.error(chatId, message));
return;
}

Long effectiveChatId = chatId;
CompletableFuture<AnswerRes> task = ragChatService.generateAnswer(chatId, member, message);

CompletableFuture<AnswerRes> task = ragChatService.generateAnswer(effectiveChatId, member, message);

taskManager.put(effectiveChatId, task);
taskManager.put(chatId, task);

task.whenComplete((result, ex) -> {
taskManager.remove(effectiveChatId);
taskManager.remove(chatId);

if (task.isCancelled() || ex != null) {

if (ex != null) {
log.error("AI 답변 생성 중 오류 발생 - chatId: {}, error: {}", effectiveChatId, ex.getMessage(), ex);
log.error("AI 답변 생성 중 오류 발생 - chatId: {}, error: {}", chatId, ex.getMessage(), ex);
} else {
log.info("AI 답변 생성 작업 취소됨 - chatId: {}", effectiveChatId);
log.info("AI 답변 생성 작업 취소됨 - chatId: {}", chatId);
}

sendNotification(effectiveChatId, member, AnswerRes.error(effectiveChatId, message));
sendNotification(chatId, member, AnswerRes.error(chatId, message));
return;
}

Expand All @@ -102,8 +92,8 @@ public void generateAnswer(Long chatId, Member member, String message) {
return;
}

log.error("AI 답변 생성 결과가 null 입니다 - chatId: {}", effectiveChatId);
sendNotification(effectiveChatId, member, AnswerRes.error(effectiveChatId, message));
log.error("AI 답변 생성 결과가 null 입니다 - chatId: {}", chatId);
sendNotification(chatId, member, AnswerRes.error(chatId, message));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ public interface ChatRepository extends JpaRepository<Chat, Long> {
@Query("""
SELECT c
FROM Chat c
LEFT JOIN Message m ON m.chat = c
JOIN Message m ON m.chat = c
WHERE c.member = :member
GROUP BY c
ORDER BY COALESCE(MAX(m.createdAt), c.createdAt) DESC
ORDER BY MAX(m.createdAt) DESC
""")
List<Chat> findAllByMemberOrderByLastMessageDesc(@Param("member") Member member);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ public abstract class SecurityConstants {
"/oauth2/**",

/* auth */
"/v1/auth/reissue",

/* websocket handshake */
"/ws/chat/**", "/ws/link/**"
"/v1/auth/reissue"
};

private static final String[] SEMI_PERMIT_URLS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,7 @@ public CorsConfigurationSource corsConfigurationSource() {
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);

CorsConfiguration webSocketConfig = new CorsConfiguration();
webSocketConfig.setAllowedOriginPatterns(List.of("*"));
webSocketConfig.setAllowedMethods(List.of("GET", "POST", "OPTIONS"));
webSocketConfig.setAllowedHeaders(List.of("*"));
webSocketConfig.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/ws/**", webSocketConfig);
source.registerCorsConfiguration("/**", config);
return source;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,29 +186,6 @@ void shouldSendNotificationWhenAnswerGeneratedSuccessfully() {
);
}

@Test
@DisplayName("채팅방 조회에 실패하면 비동기 작업을 시작하지 않고 에러 알림 전송")
void shouldSendErrorNotificationWhenChatLookupFails() {
// given
Long chatId = 999L;
String userMessage = "질문입니다";
member = mock(Member.class);
given(member.getEmail()).willReturn("test@test.com");
given(chatService.getChat(chatId, member)).willThrow(new RuntimeException("chat not found"));

// when
chatFacade.generateAnswer(chatId, member, userMessage);

// then
verify(ragChatService, never()).generateAnswer(anyLong(), any(), anyString());
verify(taskManager, never()).put(anyLong(), any());
verify(messagingTemplate).convertAndSendToUser(
eq(member.getEmail()),
eq("/queue/chat"),
any(AnswerRes.class)
);
}

@Test
@DisplayName("답변 생성 중 예외가 발생하면 에러 알림 전송")
void shouldSendErrorNotificationWhenExceptionOccurs() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.sofa.linkiving.domain.chat.integration;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -27,7 +24,6 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sofa.linkiving.domain.chat.ai.TitleClient;
import com.sofa.linkiving.domain.chat.dto.request.CreateChatReq;
import com.sofa.linkiving.domain.chat.dto.response.AnswerRes;
import com.sofa.linkiving.domain.chat.dto.response.MessageRes;
import com.sofa.linkiving.domain.chat.dto.response.MessagesRes;
import com.sofa.linkiving.domain.chat.entity.Chat;
Expand All @@ -36,7 +32,6 @@
import com.sofa.linkiving.domain.chat.facade.ChatFacade;
import com.sofa.linkiving.domain.chat.repository.ChatRepository;
import com.sofa.linkiving.domain.chat.repository.MessageRepository;
import com.sofa.linkiving.domain.chat.service.RagChatService;
import com.sofa.linkiving.domain.link.dto.response.LinkCardRes;
import com.sofa.linkiving.domain.link.entity.Link;
import com.sofa.linkiving.domain.link.entity.Summary;
Expand Down Expand Up @@ -90,9 +85,6 @@ public class ChatApiIntegrationTest {
@MockitoBean
private RedisService redisService;

@MockitoBean
private RagChatService ragChatService;

private UserDetails testUserDetails;
private Member testMember;

Expand All @@ -104,13 +96,6 @@ void setUp() {
.build());

testUserDetails = new CustomMemberDetail(testMember, Role.USER);

given(ragChatService.generateAnswer(anyLong(), any(Member.class), anyString()))
.willAnswer(invocation -> {
Long chatId = invocation.getArgument(0);
String message = invocation.getArgument(2);
return CompletableFuture.completedFuture(AnswerRes.error(chatId, message));
});
}

@Test
Expand Down Expand Up @@ -221,25 +206,6 @@ void shouldCreateChatSuccessfullyWhenValidRequest() throws Exception {
.andExpect(jsonPath("$.data.firstChat").value(firstChatContent));
}

@Test
@DisplayName("메시지가 없는 새 채팅방도 목록 조회에 포함된다")
void shouldIncludeChatWithoutMessagesInChatList() throws Exception {
// given
chatRepository.save(Chat.builder()
.member(testMember)
.title("빈 채팅방")
.build());

// when & then
mockMvc.perform(get(BASE_URL)
.with(user(testUserDetails)))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.chats").isArray())
.andExpect(jsonPath("$.data.chats[0].title").value("빈 채팅방"));
}

@Test
@DisplayName("첫 대화 내용 누락 시 400 Bad Request 반환")
void shouldReturnBadRequestWhenFirstChatIsBlank() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ void setUp() {
}

@Test
@DisplayName("빈 채팅방도 조회되며, 마지막 메시지 또는 생성 시각 기준으로 최신순 정렬됨")
void shouldReturnChatsIncludingEmptyRoomsOrderByLastActivity() throws InterruptedException {
@DisplayName("메시지가 있는 채팅방만 조회되며, 최신 메시지 순으로 정렬됨")
void shouldReturnOnlyChatsWithMessagesOrderByLastMessageTime() throws InterruptedException {
// given

// 메시지 없는 채팅방 -> 생성 시각 기준으로 함께 조회되어야
// 메시지 없는 채팅방 -> 조회되지 않아야
chatRepository.save(Chat
.builder()
.member(member)
Expand Down Expand Up @@ -90,10 +90,9 @@ void shouldReturnChatsIncludingEmptyRoomsOrderByLastActivity() throws Interrupte
List<Chat> result = chatRepository.findAllByMemberOrderByLastMessageDesc(member);

// then
assertThat(result).hasSize(3);
assertThat(result).hasSize(2);
assertThat(result.get(0).getTitle()).isEqualTo("New Msg Chat");
assertThat(result.get(1).getTitle()).isEqualTo("Old Msg Chat");
assertThat(result.get(2).getTitle()).isEqualTo("No Msg Chat");
}

@Test
Expand Down
Loading