diff --git a/src/main/java/com/sofa/linkiving/domain/chat/facade/ChatFacade.java b/src/main/java/com/sofa/linkiving/domain/chat/facade/ChatFacade.java index 60d44002..d95b4a1c 100644 --- a/src/main/java/com/sofa/linkiving/domain/chat/facade/ChatFacade.java +++ b/src/main/java/com/sofa/linkiving/domain/chat/facade/ChatFacade.java @@ -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; @@ -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 task = ragChatService.generateAnswer(chatId, member, message); - CompletableFuture 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; } @@ -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)); }); } diff --git a/src/main/java/com/sofa/linkiving/domain/chat/repository/ChatRepository.java b/src/main/java/com/sofa/linkiving/domain/chat/repository/ChatRepository.java index 3696e1af..0842b0aa 100644 --- a/src/main/java/com/sofa/linkiving/domain/chat/repository/ChatRepository.java +++ b/src/main/java/com/sofa/linkiving/domain/chat/repository/ChatRepository.java @@ -16,10 +16,10 @@ public interface ChatRepository extends JpaRepository { @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 findAllByMemberOrderByLastMessageDesc(@Param("member") Member member); diff --git a/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java b/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java index 1dbe5e9e..da1cfba3 100644 --- a/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java +++ b/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java @@ -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 = { diff --git a/src/main/java/com/sofa/linkiving/security/config/SecurityConfig.java b/src/main/java/com/sofa/linkiving/security/config/SecurityConfig.java index 0fd621b8..0d04cc99 100644 --- a/src/main/java/com/sofa/linkiving/security/config/SecurityConfig.java +++ b/src/main/java/com/sofa/linkiving/security/config/SecurityConfig.java @@ -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; } diff --git a/src/test/java/com/sofa/linkiving/domain/chat/facade/ChatFacadeTest.java b/src/test/java/com/sofa/linkiving/domain/chat/facade/ChatFacadeTest.java index add643d0..c691ffd1 100644 --- a/src/test/java/com/sofa/linkiving/domain/chat/facade/ChatFacadeTest.java +++ b/src/test/java/com/sofa/linkiving/domain/chat/facade/ChatFacadeTest.java @@ -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() { diff --git a/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java b/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java index 0686ca07..e0db1931 100644 --- a/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java +++ b/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java @@ -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; @@ -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; @@ -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; @@ -90,9 +85,6 @@ public class ChatApiIntegrationTest { @MockitoBean private RedisService redisService; - @MockitoBean - private RagChatService ragChatService; - private UserDetails testUserDetails; private Member testMember; @@ -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 @@ -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 { diff --git a/src/test/java/com/sofa/linkiving/domain/chat/repository/ChatRepositoryTest.java b/src/test/java/com/sofa/linkiving/domain/chat/repository/ChatRepositoryTest.java index 83e9abd2..45f8914c 100644 --- a/src/test/java/com/sofa/linkiving/domain/chat/repository/ChatRepositoryTest.java +++ b/src/test/java/com/sofa/linkiving/domain/chat/repository/ChatRepositoryTest.java @@ -48,11 +48,11 @@ void setUp() { } @Test - @DisplayName("빈 채팅방도 조회되며, 마지막 메시지 또는 생성 시각 기준으로 최신순 정렬됨") - void shouldReturnChatsIncludingEmptyRoomsOrderByLastActivity() throws InterruptedException { + @DisplayName("메시지가 있는 채팅방만 조회되며, 최신 메시지 순으로 정렬됨") + void shouldReturnOnlyChatsWithMessagesOrderByLastMessageTime() throws InterruptedException { // given - // 메시지 없는 채팅방 -> 생성 시각 기준으로 함께 조회되어야 함 + // 메시지 없는 채팅방 -> 조회되지 않아야 함 chatRepository.save(Chat .builder() .member(member) @@ -90,10 +90,9 @@ void shouldReturnChatsIncludingEmptyRoomsOrderByLastActivity() throws Interrupte List 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