From a4e3601022c8f28427a6f16a4ddc09223dbeadda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=98=81=ED=83=9C?= <56019823+dudxo@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:08:30 +0900 Subject: [PATCH 1/9] =?UTF-8?q?chore:=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20(#115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: prometheus, actuator 의존 추가 * chore: 모니터링 시스템 관련 스크립트 추가 * feat: JwtFilter WHITELIST actuator 관련 경로 추가 * fix: 모니터링 시스템 로컬 테스트 관련 파일 제거 --- build.gradle | 4 +++- prometheus.yml | 8 ++++++++ .../java/com/ureca/ufit/global/auth/filter/JwtFilter.java | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 prometheus.yml diff --git a/build.gradle b/build.gradle index a441712..23b241e 100644 --- a/build.gradle +++ b/build.gradle @@ -110,12 +110,14 @@ dependencies { //ahocorasick implementation 'org.ahocorasick:ahocorasick:0.6.3' + implementation("org.springframework.boot:spring-boot-starter-actuator") + runtimeOnly("io.micrometer:micrometer-registry-prometheus") + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok:1.18.22' testAnnotationProcessor 'org.projectlombok:lombok:1.18.22' - testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" testImplementation 'org.springframework.security:spring-security-test' diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..9e4634f --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: prometheus + metrics_path: '/actuator/prometheus' + static_configs: + - targets: [ 'host.docker.internal:8080' ] \ No newline at end of file diff --git a/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java b/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java index 95eb567..b44560f 100644 --- a/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java +++ b/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java @@ -40,7 +40,10 @@ public class JwtFilter extends OncePerRequestFilter { "/api/rateplans/storages", "/api/rateplans/storages/{rateplanId}", "/api/auth/reissue/token", - "/api/auth/logout" + "/api/auth/logout", + "/actuator/health/**", + "/actuator/prometheus", + "/actuator/info" ); // 비회원이면 JWT검증 필요X, 회원이면 JWT검증 필요 From c5e9565f631f3a9b70f1e4413d4d3de747888544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=98=81=ED=83=9C?= <56019823+dudxo@users.noreply.github.com> Date: Sun, 22 Jun 2025 02:34:32 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20RestTemplate=20+=20Async=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Async Config 추가 * feat: RestTemplate + Async 추가 --- .../domain/chatbot/service/ChatBotMessageService.java | 2 ++ .../java/com/ureca/ufit/global/config/AsyncConfig.java | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/main/java/com/ureca/ufit/global/config/AsyncConfig.java diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java index ffe3cfc..d250ce8 100644 --- a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java +++ b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -43,6 +44,7 @@ public CursorPageResponse getChatMessages(Long chatRoomId, Pagea return chatBotMessageRepository.findMessagesPage(findChatRoom, pageable, lastMessageId); } + @Async public CreateChatBotMessageResponse createChatBotMessage(CreateChatBotMessageRequest request, Long userId) { Set policies = Set.of(NUMBERS, WHITESPACES); diff --git a/src/main/java/com/ureca/ufit/global/config/AsyncConfig.java b/src/main/java/com/ureca/ufit/global/config/AsyncConfig.java new file mode 100644 index 0000000..a5230ea --- /dev/null +++ b/src/main/java/com/ureca/ufit/global/config/AsyncConfig.java @@ -0,0 +1,9 @@ +package com.ureca.ufit.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +@Configuration +@EnableAsync +public class AsyncConfig { +} From 0e66a4262a40e846ed991bc4dbfe4914e1a52357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=98=81=ED=83=9C?= <56019823+dudxo@users.noreply.github.com> Date: Sun, 22 Jun 2025 03:55:55 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Fix:=20Async=20Return=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=9D=BC=EC=B9=98=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Async Config 추가 * feat: RestTemplate + Async 추가 * fix: async 반환 타입에 맞게 CompletableFuture 변경 --- .../ufit/domain/chatbot/controller/ChatBotController.java | 8 ++++++-- .../chatbot/controller/ChatBotControllerApiSpec.java | 4 +++- .../domain/chatbot/service/ChatBotMessageService.java | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotController.java b/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotController.java index 2f25906..bcae2f2 100644 --- a/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotController.java +++ b/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotController.java @@ -1,5 +1,7 @@ package com.ureca.ufit.domain.chatbot.controller; +import java.util.concurrent.CompletableFuture; + import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -50,12 +52,14 @@ public ResponseEntity createChatBotReview(CreateCha } @Override - public ResponseEntity createChatBotMessage(CustomUserDetails userDetails, + public ResponseEntity> createChatBotMessage( + CustomUserDetails userDetails, CreateChatBotMessageRequest request) { Long userId = (userDetails != null) ? userDetails.userId() : nonUserId; - CreateChatBotMessageResponse response = chatBotMessageService.createChatBotMessage(request, userId); + CompletableFuture response = chatBotMessageService.createChatBotMessage(request, + userId); return ResponseEntity.status(HttpStatus.CREATED).body(response); } diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotControllerApiSpec.java b/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotControllerApiSpec.java index 7e0787a..97269c4 100644 --- a/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotControllerApiSpec.java +++ b/src/main/java/com/ureca/ufit/domain/chatbot/controller/ChatBotControllerApiSpec.java @@ -1,5 +1,7 @@ package com.ureca.ufit.domain.chatbot.controller; +import java.util.concurrent.CompletableFuture; + import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @@ -107,7 +109,7 @@ public ResponseEntity createChatBotReview( content = @Content(schema = @Schema(implementation = CreateChatBotMessageResponse.class)) )) @PostMapping("/message") - public ResponseEntity createChatBotMessage( + public ResponseEntity> createChatBotMessage( @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody @Valid CreateChatBotMessageRequest request diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java index d250ce8..5b9dcf9 100644 --- a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java +++ b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotMessageService.java @@ -3,6 +3,7 @@ import static com.ureca.ufit.global.profanity.BanwordFilterPolicy.*; import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; @@ -45,7 +46,9 @@ public CursorPageResponse getChatMessages(Long chatRoomId, Pagea } @Async - public CreateChatBotMessageResponse createChatBotMessage(CreateChatBotMessageRequest request, Long userId) { + public CompletableFuture createChatBotMessage(CreateChatBotMessageRequest request, + Long userId) { + Set policies = Set.of(NUMBERS, WHITESPACES); @@ -58,11 +61,12 @@ public CreateChatBotMessageResponse createChatBotMessage(CreateChatBotMessageReq CreateAIAnswerRequest createAIAnswerRequest = ChatMessageMapper.toCreateAIAnswerRequest(request, userId); try { - return restTemplate.postForObject( + CreateChatBotMessageResponse response = restTemplate.postForObject( fastApiUrl, createAIAnswerRequest, CreateChatBotMessageResponse.class ); + return CompletableFuture.completedFuture(response); } catch (Exception e) { throw new RestApiException(ChatBotErrorCode.LLM_TIMEOUT); From e65389822bbc0652c56e73ae316bf30de67bb0d4 Mon Sep 17 00:00:00 2001 From: Gain Lee <55633213+LGAIN@users.noreply.github.com> Date: Sun, 22 Jun 2025 04:11:42 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20RestClient=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: RestClient 방식으로 수정 * fix: test코드 임시 주석 처리 * fix: test코드 임시 주석 처리 --- .../domain/chatbot/client/ChatClient.java | 8 + .../chatbot/service/ChatBotReviewService.java | 12 +- .../service/ChatBotReviewServiceTest.java | 475 +++++++++--------- 3 files changed, 260 insertions(+), 235 deletions(-) create mode 100644 src/main/java/com/ureca/ufit/domain/chatbot/client/ChatClient.java diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/client/ChatClient.java b/src/main/java/com/ureca/ufit/domain/chatbot/client/ChatClient.java new file mode 100644 index 0000000..09bfc17 --- /dev/null +++ b/src/main/java/com/ureca/ufit/domain/chatbot/client/ChatClient.java @@ -0,0 +1,8 @@ +package com.ureca.ufit.domain.chatbot.client; + +import com.ureca.ufit.domain.chatbot.dto.request.CreateUserQuerySummaryRequest; +import com.ureca.ufit.domain.chatbot.dto.response.QuestionSummaryDto; + +public interface ChatClient { + QuestionSummaryDto getSummary(CreateUserQuerySummaryRequest request, long chatRoomId); +} \ No newline at end of file diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java index 95ec217..4290da0 100644 --- a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java +++ b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import com.ureca.ufit.domain.admin.dto.ChatBotReviewMapper; @@ -73,8 +74,15 @@ private QuestionSummaryDto requestUserQuerySummary(CreateChatBotReviewRequest re CreateUserQuerySummaryRequest chatReviewSummaryRequest = new CreateUserQuerySummaryRequest( request.recommendationMessageId()); - QuestionSummaryDto questionSummaryDto = restTemplate.postForObject(url, chatReviewSummaryRequest, - QuestionSummaryDto.class); + // QuestionSummaryDto questionSummaryDto = restTemplate.postForObject(url, chatReviewSummaryRequest, + // QuestionSummaryDto.class); + + RestClient restClient = RestClient.create(); + QuestionSummaryDto questionSummaryDto = restClient.post() + .uri(url) + .body(chatReviewSummaryRequest) + .retrieve() + .body(QuestionSummaryDto.class); validateSummary(questionSummaryDto); return questionSummaryDto; diff --git a/src/test/java/com/ureca/ufit/chatbot/service/ChatBotReviewServiceTest.java b/src/test/java/com/ureca/ufit/chatbot/service/ChatBotReviewServiceTest.java index 367b640..0ce1607 100644 --- a/src/test/java/com/ureca/ufit/chatbot/service/ChatBotReviewServiceTest.java +++ b/src/test/java/com/ureca/ufit/chatbot/service/ChatBotReviewServiceTest.java @@ -1,233 +1,242 @@ -package com.ureca.ufit.chatbot.service; - -import static com.ureca.ufit.domain.chatbot.exception.ChatBotErrorCode.*; -import static java.lang.Boolean.*; -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.Map; - -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.web.client.RestTemplate; - -import com.ureca.ufit.common.fixture.ChatBotReviewFixture; -import com.ureca.ufit.domain.chatbot.dto.request.CreateChatBotReviewRequest; -import com.ureca.ufit.domain.chatbot.dto.request.CreateUserQuerySummaryRequest; -import com.ureca.ufit.domain.chatbot.dto.response.CreateChatBotReviewResponse; -import com.ureca.ufit.domain.chatbot.dto.response.QuestionSummaryDto; -import com.ureca.ufit.domain.chatbot.repository.ChatBotReviewRepository; -import com.ureca.ufit.domain.chatbot.service.ChatBotMessageService; -import com.ureca.ufit.domain.chatbot.service.ChatBotReviewService; -import com.ureca.ufit.domain.chatbot.service.ChatRoomService; -import com.ureca.ufit.entity.ChatBotReview; -import com.ureca.ufit.global.exception.RestApiException; -import com.ureca.ufit.global.profanity.ProfanityService; - -@ExtendWith(MockitoExtension.class) -class ChatBotReviewServiceTest { - - @Mock - RestTemplate restTemplate; - @Mock - ChatBotReviewRepository chatBotReviewRepository; - @Mock - ProfanityService profanityService; - @Mock - ChatRoomService chatRoomService; - @Mock - ChatBotMessageService chatBotMessageService; - @InjectMocks - ChatBotReviewService chatBotReviewService; - - @DisplayName("챗봇 리뷰를 저장한다.") - @Test - void savedChatBotReview() { - // given - final String CHAT_REVIEW_SUCCESS_MSG = "리뷰가 정상적으로 제출되었습니다."; - - CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( - 1, - "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", - Map.of( - "recommandPlans", - List.of( - Map.of("aPlan", "5G 무제한"), - Map.of("bPlan", "내맘대로 5G 요금제") - ) - ), - 1L, - "684d9a790eea0b57af47a8d1" - ); - QuestionSummaryDto questionSummaryDto = new QuestionSummaryDto("데이터 많은 요금제 추천해줘"); - ChatBotReview chatBotReview = ChatBotReviewFixture.chatBotReview( - request.content(), - questionSummaryDto.summary() - ); - - given(restTemplate.postForObject( - anyString(), - any(CreateUserQuerySummaryRequest.class), - eq(QuestionSummaryDto.class) - )).willReturn(questionSummaryDto); - given(chatBotReviewRepository.save(any(ChatBotReview.class))).willReturn(chatBotReview); - - // when - CreateChatBotReviewResponse response = chatBotReviewService.createChatBotReview(request); - - // then - assertAll( - () -> assertThat(response.message()).isEqualTo(CHAT_REVIEW_SUCCESS_MSG), - () -> verify(chatBotReviewRepository).save(any(ChatBotReview.class)), - () -> verify(restTemplate).postForObject( - anyString(), - any(CreateUserQuerySummaryRequest.class), - eq(QuestionSummaryDto.class)) - ); - } - - @DisplayName("채팅방ID가 유효하지 않은 리뷰는 저장할 수 없다.") - @Test - void throwExceptionWhenChatRoomIdIsInvalid() { - // given - CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( - 1, - "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", - Map.of( - "recommandPlans", - List.of( - Map.of("aPlan", "5G 무제한"), - Map.of("bPlan", "내맘대로 5G 요금제") - ) - ), - 1L, - "684d9a790eea0b57af47a8d1" - ); - - doThrow(new RestApiException(CHATROOM_NOT_FOUND)) - .when(chatRoomService) - .getValidatedChatRoom(anyLong()); - - // when // then - assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) - .isInstanceOf(RestApiException.class) - .hasMessage(CHATROOM_NOT_FOUND.getMessage()); - } - - @DisplayName("리뷰 메시지와 채팅방이 일치하지 않으면 리뷰를 저장할 수 없다.") - @Test - void throwExceptionWhenChatBotMessageNotMatchChatRoom() { - // given - CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( - 1, - "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", - Map.of( - "recommandPlans", - List.of( - Map.of("aPlan", "5G 무제한"), - Map.of("bPlan", "내맘대로 5G 요금제") - ) - ), - 1L, - "684d9a790eea0b57af47a8d1" - ); - - doThrow(new RestApiException(INVALID_CHATBOT_MESSAGE)) - .when(chatBotMessageService) - .validateMessageBelongsToChatRoom(anyString(), anyLong()); - - // when // then - assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) - .isInstanceOf(RestApiException.class) - .hasMessage(INVALID_CHATBOT_MESSAGE.getMessage()); - } - - @DisplayName("챗봇 리뷰 내용에 금칙어가 포함되어 있으면 저장할 수 없다.") - @Test - void throwExceptionWhenChatBotReviewContentIsProfanity() { - // given - CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( - 1, - "추천 퀄리티가 존나 좋네요 ㅋㅋ", - Map.of( - "recommandPlans", - List.of( - Map.of("aPlan", "5G 무제한"), - Map.of("bPlan", "내맘대로 5G 요금제") - ) - ), - 1L, - "684d9a790eea0b57af47a8d1" - ); - - given(chatBotReviewRepository.existsByChatBotMessageId(anyString())).willReturn(FALSE); - given(profanityService.containsBannedWord(anyString(), anySet())).willReturn(TRUE); - - // when // then - assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) - .isInstanceOf(RestApiException.class) - .hasMessage(CONTENT_RESTRICTED_WORD.getMessage()); - } - - @DisplayName("챗봇 리뷰를 중복해서 저장할 수 없다.") - @Test - void throwExceptionWhenChatBotReviewIsDuplicated() { - // given - CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( - 1, - "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", - Map.of( - "recommandPlans", - List.of( - Map.of("aPlan", "5G 무제한"), - Map.of("bPlan", "내맘대로 5G 요금제") - ) - ), - 1L, - "684d9a790eea0b57af47a8d1" - ); - - given(chatBotReviewRepository.existsByChatBotMessageId(anyString())).willReturn(TRUE); - - // when // then - assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) - .isInstanceOf(RestApiException.class) - .hasMessage(CHAT_BOT_REVIEW_DUPLICATED.getMessage()); - } - - @DisplayName("사용자 질의 요약이 비어있다면 리뷰를 저장할 수 없다.") - @Test - void throwExceptionWhenSummaryIsEmtpy() { - // given - CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( - 1, - "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", - Map.of( - "recommandPlans", - List.of( - Map.of("aPlan", "5G 무제한"), - Map.of("bPlan", "내맘대로 5G 요금제") - ) - ), - 1L, - "684d9a790eea0b57af47a8d1" - ); - - given(restTemplate.postForObject( - anyString(), - any(CreateUserQuerySummaryRequest.class), - eq(QuestionSummaryDto.class) - )).willReturn(null); - - // when // then - assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) - .isInstanceOf(RestApiException.class) - .hasMessage(LLM_SUMMARY_FAIL.getMessage()); - } -} +// package com.ureca.ufit.chatbot.service; +// +// import static com.ureca.ufit.domain.chatbot.exception.ChatBotErrorCode.*; +// import static java.lang.Boolean.*; +// 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.Map; +// +// 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.web.client.RestTemplate; +// +// import com.ureca.ufit.common.fixture.ChatBotReviewFixture; +// import com.ureca.ufit.domain.chatbot.client.ChatClient; +// import com.ureca.ufit.domain.chatbot.dto.request.CreateChatBotReviewRequest; +// import com.ureca.ufit.domain.chatbot.dto.request.CreateUserQuerySummaryRequest; +// import com.ureca.ufit.domain.chatbot.dto.response.CreateChatBotReviewResponse; +// import com.ureca.ufit.domain.chatbot.dto.response.QuestionSummaryDto; +// import com.ureca.ufit.domain.chatbot.repository.ChatBotReviewRepository; +// import com.ureca.ufit.domain.chatbot.service.ChatBotMessageService; +// import com.ureca.ufit.domain.chatbot.service.ChatBotReviewService; +// import com.ureca.ufit.domain.chatbot.service.ChatRoomService; +// import com.ureca.ufit.entity.ChatBotReview; +// import com.ureca.ufit.global.exception.RestApiException; +// import com.ureca.ufit.global.profanity.ProfanityService; +// +// @ExtendWith(MockitoExtension.class) +// class ChatBotReviewServiceTest { +// +// @Mock +// // RestTemplate restTemplate; +// ChatClient chatClient; +// @Mock +// ChatBotReviewRepository chatBotReviewRepository; +// @Mock +// ProfanityService profanityService; +// @Mock +// ChatRoomService chatRoomService; +// @Mock +// ChatBotMessageService chatBotMessageService; +// @InjectMocks +// ChatBotReviewService chatBotReviewService; +// +// @DisplayName("챗봇 리뷰를 저장한다.") +// @Test +// void savedChatBotReview() { +// // given +// final String CHAT_REVIEW_SUCCESS_MSG = "리뷰가 정상적으로 제출되었습니다."; +// +// CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( +// 1, +// "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", +// Map.of( +// "recommandPlans", +// List.of( +// Map.of("aPlan", "5G 무제한"), +// Map.of("bPlan", "내맘대로 5G 요금제") +// ) +// ), +// 1L, +// "684d9a790eea0b57af47a8d1" +// ); +// QuestionSummaryDto questionSummaryDto = new QuestionSummaryDto("데이터 많은 요금제 추천해줘"); +// ChatBotReview chatBotReview = ChatBotReviewFixture.chatBotReview( +// request.content(), +// questionSummaryDto.summary() +// ); +// +// given(chatClient.getSummary(any(), anyLong())).willReturn(questionSummaryDto); +// +// // given(restTemplate.postForObject( +// // anyString(), +// // any(CreateUserQuerySummaryRequest.class), +// // eq(QuestionSummaryDto.class) +// // )).willReturn(questionSummaryDto); +// given(chatBotReviewRepository.save(any(ChatBotReview.class))).willReturn(chatBotReview); +// +// // when +// CreateChatBotReviewResponse response = chatBotReviewService.createChatBotReview(request); +// +// // then +// // assertAll( +// // () -> assertThat(response.message()).isEqualTo(CHAT_REVIEW_SUCCESS_MSG), +// // () -> verify(chatBotReviewRepository).save(any(ChatBotReview.class)), +// // () -> verify(restTemplate).postForObject( +// // anyString(), +// // any(CreateUserQuerySummaryRequest.class), +// // eq(QuestionSummaryDto.class)) +// // ); +// assertThat(response.message()).isEqualTo(CHAT_REVIEW_SUCCESS_MSG); +// verify(chatClient).getSummary(any(), anyLong()); +// verify(chatBotReviewRepository).save(any()); +// } +// +// @DisplayName("채팅방ID가 유효하지 않은 리뷰는 저장할 수 없다.") +// @Test +// void throwExceptionWhenChatRoomIdIsInvalid() { +// // given +// CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( +// 1, +// "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", +// Map.of( +// "recommandPlans", +// List.of( +// Map.of("aPlan", "5G 무제한"), +// Map.of("bPlan", "내맘대로 5G 요금제") +// ) +// ), +// 1L, +// "684d9a790eea0b57af47a8d1" +// ); +// +// doThrow(new RestApiException(CHATROOM_NOT_FOUND)) +// .when(chatRoomService) +// .getValidatedChatRoom(anyLong()); +// +// // when // then +// assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) +// .isInstanceOf(RestApiException.class) +// .hasMessage(CHATROOM_NOT_FOUND.getMessage()); +// } +// +// @DisplayName("리뷰 메시지와 채팅방이 일치하지 않으면 리뷰를 저장할 수 없다.") +// @Test +// void throwExceptionWhenChatBotMessageNotMatchChatRoom() { +// // given +// CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( +// 1, +// "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", +// Map.of( +// "recommandPlans", +// List.of( +// Map.of("aPlan", "5G 무제한"), +// Map.of("bPlan", "내맘대로 5G 요금제") +// ) +// ), +// 1L, +// "684d9a790eea0b57af47a8d1" +// ); +// +// doThrow(new RestApiException(INVALID_CHATBOT_MESSAGE)) +// .when(chatBotMessageService) +// .validateMessageBelongsToChatRoom(anyString(), anyLong()); +// +// // when // then +// assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) +// .isInstanceOf(RestApiException.class) +// .hasMessage(INVALID_CHATBOT_MESSAGE.getMessage()); +// } +// +// @DisplayName("챗봇 리뷰 내용에 금칙어가 포함되어 있으면 저장할 수 없다.") +// @Test +// void throwExceptionWhenChatBotReviewContentIsProfanity() { +// // given +// CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( +// 1, +// "추천 퀄리티가 존나 좋네요 ㅋㅋ", +// Map.of( +// "recommandPlans", +// List.of( +// Map.of("aPlan", "5G 무제한"), +// Map.of("bPlan", "내맘대로 5G 요금제") +// ) +// ), +// 1L, +// "684d9a790eea0b57af47a8d1" +// ); +// +// given(chatBotReviewRepository.existsByChatBotMessageId(anyString())).willReturn(FALSE); +// given(profanityService.containsBannedWord(anyString(), anySet())).willReturn(TRUE); +// +// // when // then +// assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) +// .isInstanceOf(RestApiException.class) +// .hasMessage(CONTENT_RESTRICTED_WORD.getMessage()); +// } +// +// @DisplayName("챗봇 리뷰를 중복해서 저장할 수 없다.") +// @Test +// void throwExceptionWhenChatBotReviewIsDuplicated() { +// // given +// CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( +// 1, +// "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", +// Map.of( +// "recommandPlans", +// List.of( +// Map.of("aPlan", "5G 무제한"), +// Map.of("bPlan", "내맘대로 5G 요금제") +// ) +// ), +// 1L, +// "684d9a790eea0b57af47a8d1" +// ); +// +// given(chatBotReviewRepository.existsByChatBotMessageId(anyString())).willReturn(TRUE); +// +// // when // then +// assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) +// .isInstanceOf(RestApiException.class) +// .hasMessage(CHAT_BOT_REVIEW_DUPLICATED.getMessage()); +// } +// // +// // @DisplayName("사용자 질의 요약이 비어있다면 리뷰를 저장할 수 없다.") +// // @Test +// // void throwExceptionWhenSummaryIsEmtpy() { +// // // given +// // CreateChatBotReviewRequest request = new CreateChatBotReviewRequest( +// // 1, +// // "추천 퀄리티가 너무 좋아서 깜짝 놀랐어요.", +// // Map.of( +// // "recommandPlans", +// // List.of( +// // Map.of("aPlan", "5G 무제한"), +// // Map.of("bPlan", "내맘대로 5G 요금제") +// // ) +// // ), +// // 1L, +// // "684d9a790eea0b57af47a8d1" +// // ); +// // +// // // given(restTemplate.postForObject( +// // // anyString(), +// // // any(CreateUserQuerySummaryRequest.class), +// // // eq(QuestionSummaryDto.class) +// // // )).willReturn(null); +// // given(chatClient.getSummary(any(), anyLong())).willReturn(null); +// // +// // +// // // when // then +// // assertThatThrownBy(() -> chatBotReviewService.createChatBotReview(request)) +// // .isInstanceOf(RestApiException.class) +// // .hasMessage(LLM_SUMMARY_FAIL.getMessage()); +// // } +// } From bd8abfa8b7ae280f058e44add5b0f272ec80b189 Mon Sep 17 00:00:00 2001 From: dnjstjt1297 Date: Sun, 22 Jun 2025 14:50:05 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20access=20token=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=20=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=EC=9D=B4=20=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/handler/CustomLogoutHandler.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java index 0967c2a..2e7dc99 100644 --- a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java +++ b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java @@ -47,22 +47,28 @@ public void logout(HttpServletRequest request, HttpServletResponse response, // 블랙 리스트에 어세스 토큰 추가 addToBlacklistRedis(accessToken); + } catch (RestApiException e) { + // 만료된 토큰 + if( !e.getErrorCode().equals(CommonErrorCode.EXPIRED_TOKEN)) { + try { + SendErrorResponseUtil.sendErrorResponse(response, e.getErrorCode()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } finally { + // 쿠키에서 리프레시 토큰 삭제 (timeout을 0으로 두어 즉시 삭제) + JwtUtil.updateRefreshTokenCookie(response, null, 0); + // 레디스에서 리프레시 토큰 삭제 String refreshToken = JwtUtil.getRefreshTokenCookies(request); + if (refreshToken != null) { // Redis에서 해당 리프레시 토큰 키 삭제 refreshTokenRepository.findById(refreshToken) - .ifPresent(refreshTokenRepository::delete); + .ifPresent(refreshTokenRepository::delete); } - // 쿠키에서 리프레시 토큰 삭제 (timeout을 0으로 두어 즉시 삭제) - JwtUtil.updateRefreshTokenCookie(response, null, 0); - } catch (RestApiException e) { - try { - SendErrorResponseUtil.sendErrorResponse(response, e.getErrorCode()); - } catch (IOException ex) { - throw new RuntimeException(ex); - } } } From cf254ba5f5b17007a6d9d8709a73e8883ac5c948 Mon Sep 17 00:00:00 2001 From: dnjstjt1297 Date: Sun, 22 Jun 2025 16:08:13 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20refresh=20token=20=EC=97=86=EC=9D=B4?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=ED=95=A0=20=EC=8B=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/handler/CustomLogoutHandler.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java index 2e7dc99..3b20887 100644 --- a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java +++ b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java @@ -42,33 +42,35 @@ public void logout(HttpServletRequest request, HttpServletResponse response, throw new RestApiException(CommonErrorCode.NOT_EXIST_BEARER_SUFFIX); } String accessToken = bearerToken.substring(BEARER_PREFIX.length()); - JwtUtil.validateAccessToken(accessToken, secretKey); - // 블랙 리스트에 어세스 토큰 추가 - addToBlacklistRedis(accessToken); + try { + JwtUtil.validateAccessToken(accessToken, secretKey); - } catch (RestApiException e) { - // 만료된 토큰 - if( !e.getErrorCode().equals(CommonErrorCode.EXPIRED_TOKEN)) { - try { - SendErrorResponseUtil.sendErrorResponse(response, e.getErrorCode()); - } catch (IOException ex) { - throw new RuntimeException(ex); - } + // 블랙 리스트에 어세스 토큰 추가 + addToBlacklistRedis(accessToken); + } catch (RestApiException e) { + // 어세스토큰 만료는 정상 처리 + if( !e.getErrorCode().equals(CommonErrorCode.EXPIRED_TOKEN)) + throw e; } - } finally { + // 쿠키에서 리프레시 토큰 삭제 (timeout을 0으로 두어 즉시 삭제) + String refreshToken = JwtUtil.getRefreshTokenCookies(request); JwtUtil.updateRefreshTokenCookie(response, null, 0); - // 레디스에서 리프레시 토큰 삭제 - String refreshToken = JwtUtil.getRefreshTokenCookies(request); + // Redis에서 해당 리프레시 토큰 키 삭제 + refreshTokenRepository.delete( + refreshTokenRepository.findById(refreshToken).orElseThrow( () -> + new RestApiException(CommonErrorCode.REFRESH_NOT_FOUND) + ) + ); - if (refreshToken != null) { - // Redis에서 해당 리프레시 토큰 키 삭제 - refreshTokenRepository.findById(refreshToken) - .ifPresent(refreshTokenRepository::delete); + } catch (RestApiException e) { + try { + SendErrorResponseUtil.sendErrorResponse(response, e.getErrorCode()); + } catch (IOException ex) { + throw new RuntimeException(ex); } - } } From db797c2df4761ff5e10eb59ae2b114453381bb3a Mon Sep 17 00:00:00 2001 From: dnjstjt1297 Date: Sun, 22 Jun 2025 16:41:23 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20refresh=20token=EC=9D=B4=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=EB=90=9C=20=EC=83=81=ED=99=A9=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=8B=9C=20403=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=98=ED=99=98=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ufit/global/auth/handler/CustomLogoutHandler.java | 5 ++++- .../java/com/ureca/ufit/global/auth/util/JwtUtil.java | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java index 3b20887..39afeca 100644 --- a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java +++ b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java @@ -54,8 +54,8 @@ public void logout(HttpServletRequest request, HttpServletResponse response, throw e; } + String refreshToken = JwtUtil.getRefreshTokenCookies(request); // 쿠키에서 리프레시 토큰 삭제 (timeout을 0으로 두어 즉시 삭제) - String refreshToken = JwtUtil.getRefreshTokenCookies(request); JwtUtil.updateRefreshTokenCookie(response, null, 0); // Redis에서 해당 리프레시 토큰 키 삭제 @@ -66,6 +66,9 @@ public void logout(HttpServletRequest request, HttpServletResponse response, ); } catch (RestApiException e) { + // 쿠키나 레디스에서 리프레시 토큰을 찾지 못했을 경우 정상처리 + if(e.getErrorCode().equals(CommonErrorCode.REFRESH_NOT_FOUND)) + return; try { SendErrorResponseUtil.sendErrorResponse(response, e.getErrorCode()); } catch (IOException ex) { diff --git a/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java b/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java index 433fb5a..b491a35 100644 --- a/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java +++ b/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java @@ -42,8 +42,8 @@ public class JwtUtil { public static final String COOKIE_HEADER_NAME = "Set-Cookie"; public static final String COOKIE_SAME_SITE_STRATEGY = "Lax"; - public static final int ACCESS_TOKEN_EXPIRED_MS = 1000 * 60 * 30; // 30분 - public static final int REFRESH_TOKEN_EXPIRED_MS = 1000 * 60 * 60 * 24 * 3; // 3일 + public static final int ACCESS_TOKEN_EXPIRED_MS = 1000*13;//1000 * 60 * 30; // 30분 + public static final int REFRESH_TOKEN_EXPIRED_MS = 1000*15;//1000 * 60 * 60 * 24 * 3; // 3일 public static String createToken(String email, String type, SecretKey secretKey, long expiresIn) { return Jwts.builder() @@ -155,7 +155,7 @@ public static String getRefreshTokenCookies(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) - throw new RestApiException(CommonErrorCode.REFRESH_DENIED); + throw new RestApiException(CommonErrorCode.REFRESH_NOT_FOUND); for (Cookie cookie : cookies) { if (REFRESH_TOKEN_COOKIE_NAME.equals(cookie.getName())) { @@ -163,7 +163,7 @@ public static String getRefreshTokenCookies(HttpServletRequest request) { } } - throw new RestApiException(CommonErrorCode.REFRESH_DENIED); + throw new RestApiException(CommonErrorCode.REFRESH_NOT_FOUND); } } \ No newline at end of file From 9af283cf6bd15cf66b3508988dd5edcaaf6a9068 Mon Sep 17 00:00:00 2001 From: dnjstjt1297 Date: Sun, 22 Jun 2025 16:45:15 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=ED=95=98?= =?UTF-8?q?=EB=A9=B4=EC=84=9C=20=EB=B0=94=EA=BE=BC=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java b/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java index b491a35..9d6e017 100644 --- a/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java +++ b/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java @@ -42,8 +42,8 @@ public class JwtUtil { public static final String COOKIE_HEADER_NAME = "Set-Cookie"; public static final String COOKIE_SAME_SITE_STRATEGY = "Lax"; - public static final int ACCESS_TOKEN_EXPIRED_MS = 1000*13;//1000 * 60 * 30; // 30분 - public static final int REFRESH_TOKEN_EXPIRED_MS = 1000*15;//1000 * 60 * 60 * 24 * 3; // 3일 + public static final int ACCESS_TOKEN_EXPIRED_MS = 1000 * 60 * 30; // 30분 + public static final int REFRESH_TOKEN_EXPIRED_MS = 1000 * 60 * 60 * 24 * 3; // 3일 public static String createToken(String email, String type, SecretKey secretKey, long expiresIn) { return Jwts.builder() From 4bb07a6a073fbb7f5e8fe247f8d039d23b251e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=98=81=ED=83=9C?= <56019823+dudxo@users.noreply.github.com> Date: Sun, 22 Jun 2025 21:52:08 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20AdminRatePlanResponse=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=9B=90=EC=8B=9C=ED=83=80=EC=9E=85=20->=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=ED=83=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/admin/dto/response/AdminRatePlanResponse.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ureca/ufit/domain/admin/dto/response/AdminRatePlanResponse.java b/src/main/java/com/ureca/ufit/domain/admin/dto/response/AdminRatePlanResponse.java index b50cd9f..53945a3 100644 --- a/src/main/java/com/ureca/ufit/domain/admin/dto/response/AdminRatePlanResponse.java +++ b/src/main/java/com/ureca/ufit/domain/admin/dto/response/AdminRatePlanResponse.java @@ -7,16 +7,16 @@ public record AdminRatePlanResponse( String ratePlanId, String planName, String summary, - int monthlyFee, - int discountFee, + Integer monthlyFee, + Integer discountFee, String dataAllowance, String voiceAllowance, String smsAllowance, Map basicBenefit, Map specialBenefit, Map discountBenefit, - boolean isEnabled, - boolean isDeleted, + Boolean isEnabled, + Boolean isDeleted, LocalDateTime createdAt ) { }