diff --git a/build.gradle b/build.gradle index d70acd82..be1e284c 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ apply from: "gradle/devtool.gradle" apply from: "gradle/test.gradle" apply from: "gradle/sonar.gradle" apply from: "gradle/db.gradle" +apply from: "gradle/sentry.gradle" allprojects { diff --git a/gradle/apm.gradle b/gradle/sentry.gradle similarity index 100% rename from gradle/apm.gradle rename to gradle/sentry.gradle diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index c1afbee9..bafc30f2 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -70,8 +70,10 @@ public FriendsResponse findFriends(@PathVariable("userId") Long userId) { } @GetMapping("/interests") - public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") List userIds) { - return userService.getInterestQuestionByUserIds(userIds); + @ResponseStatus(HttpStatus.OK) + public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") List userIds, + @RequestParam("type") String balance) { + return userService.getInterestQuestionByUserIds(userIds, balance); } @DeleteMapping("/withdraw") diff --git a/src/main/java/net/teumteum/user/domain/BalanceGameType.java b/src/main/java/net/teumteum/user/domain/BalanceGameType.java new file mode 100644 index 00000000..6c3e66fd --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/BalanceGameType.java @@ -0,0 +1,34 @@ +package net.teumteum.user.domain; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import net.teumteum.user.domain.response.InterestQuestionResponse; + +public enum BalanceGameType { + + BALANCE("balance", (users, interestQuestion) -> interestQuestion.getBalanceGame(users)), + STORY("story", (users, interestQuestion) -> interestQuestion.getStoryGame(users)), + ; + + private final String value; + private final BiFunction, InterestQuestion, InterestQuestionResponse> behavior; + + BalanceGameType(String value, BiFunction, InterestQuestion, InterestQuestionResponse> behavior) { + this.value = value; + this.behavior = behavior; + } + + public static BalanceGameType of(String value) { + return Arrays.stream(BalanceGameType.values()) + .filter(type -> type.value.equals(value)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException("\"" + value + "\" 에 해당하는 enum값을 찾을 수 없습니다.") + ); + } + + public InterestQuestionResponse getInterestQuestionResponse(List users, InterestQuestion interestQuestion) { + return behavior.apply(users, interestQuestion); + } +} diff --git a/src/main/java/net/teumteum/user/domain/InterestQuestion.java b/src/main/java/net/teumteum/user/domain/InterestQuestion.java index 9450c239..66b97e9f 100644 --- a/src/main/java/net/teumteum/user/domain/InterestQuestion.java +++ b/src/main/java/net/teumteum/user/domain/InterestQuestion.java @@ -1,11 +1,13 @@ package net.teumteum.user.domain; import java.util.List; -import net.teumteum.user.domain.response.InterestQuestionResponse; +import net.teumteum.user.domain.response.BalanceQuestionResponse; +import net.teumteum.user.domain.response.StoryQuestionResponse; -@FunctionalInterface public interface InterestQuestion { - InterestQuestionResponse getQuestion(List users); + BalanceQuestionResponse getBalanceGame(List users); + + StoryQuestionResponse getStoryGame(List users); } diff --git a/src/main/java/net/teumteum/user/domain/response/BalanceQuestionResponse.java b/src/main/java/net/teumteum/user/domain/response/BalanceQuestionResponse.java new file mode 100644 index 00000000..c9a6c0cf --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/response/BalanceQuestionResponse.java @@ -0,0 +1,10 @@ +package net.teumteum.user.domain.response; + +import java.util.List; + +public record BalanceQuestionResponse( + String topic, + List balanceQuestion +) implements InterestQuestionResponse { + +} diff --git a/src/main/java/net/teumteum/user/domain/response/InterestQuestionResponse.java b/src/main/java/net/teumteum/user/domain/response/InterestQuestionResponse.java index 22921f57..46516ad1 100644 --- a/src/main/java/net/teumteum/user/domain/response/InterestQuestionResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/InterestQuestionResponse.java @@ -1,10 +1,5 @@ package net.teumteum.user.domain.response; -import java.util.List; - -public record InterestQuestionResponse( - String topic, - List balanceQuestion -) { +public interface InterestQuestionResponse { } diff --git a/src/main/java/net/teumteum/user/domain/response/StoryQuestionResponse.java b/src/main/java/net/teumteum/user/domain/response/StoryQuestionResponse.java new file mode 100644 index 00000000..507b8861 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/response/StoryQuestionResponse.java @@ -0,0 +1,8 @@ +package net.teumteum.user.domain.response; + +public record StoryQuestionResponse( + String topic, + String story +) implements InterestQuestionResponse { + +} diff --git a/src/main/java/net/teumteum/user/infra/GptInterestQuestion.java b/src/main/java/net/teumteum/user/infra/GptInterestQuestion.java index 6834b064..8ce36994 100644 --- a/src/main/java/net/teumteum/user/infra/GptInterestQuestion.java +++ b/src/main/java/net/teumteum/user/infra/GptInterestQuestion.java @@ -10,7 +10,8 @@ import lombok.RequiredArgsConstructor; import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.User; -import net.teumteum.user.domain.response.InterestQuestionResponse; +import net.teumteum.user.domain.response.BalanceQuestionResponse; +import net.teumteum.user.domain.response.StoryQuestionResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; @@ -32,16 +33,33 @@ public class GptInterestQuestion implements InterestQuestion { @Override - public InterestQuestionResponse getQuestion(List users) { + public BalanceQuestionResponse getBalanceGame(List users) { var interests = parseInterests(users); - var request = GptQuestionRequest.of(interests); + var request = GptQuestionRequest.balanceGame(interests); return webClient.post() .bodyValue(request) .header(HttpHeaders.AUTHORIZATION, gptToken) .exchangeToMono(response -> { if (response.statusCode().is2xxSuccessful()) { - return response.bodyToMono(InterestQuestionResponse.class); + return response.bodyToMono(BalanceQuestionResponse.class); + } + return response.createError(); + }) + .retry(MAX_RETRY_COUNT) + .subscribeOn(Schedulers.fromExecutor(executorService)) + .block(Duration.ofSeconds(5)); + } + + @Override + public StoryQuestionResponse getStoryGame(List users) { + var interests = parseInterests(users); + var request = GptQuestionRequest.story(interests); + + return webClient.post().bodyValue(request).header(HttpHeaders.AUTHORIZATION, gptToken) + .exchangeToMono(response -> { + if (response.statusCode().is2xxSuccessful()) { + return response.bodyToMono(StoryQuestionResponse.class); } return response.createError(); }) @@ -72,29 +90,24 @@ private record GptQuestionRequest( private static final String LANGUAGE_MODEL = "gpt-3.5-turbo-1106"; - private static GptQuestionRequest of(String interests) { - return new GptQuestionRequest( - LANGUAGE_MODEL, - List.of(Message.system(), Message.user(interests)) - ); + private static GptQuestionRequest balanceGame(String interests) { + return new GptQuestionRequest(LANGUAGE_MODEL, List.of(Message.balanceGame(), Message.user(interests))); } - private record Message( - String role, - String content - ) { + private static GptQuestionRequest story(String interests) { + return new GptQuestionRequest(LANGUAGE_MODEL, List.of(Message.story(), Message.user(interests))); + } - private static Message system() { - return new Message( - "system", - "You are a chatbot that receives the user's interests and creates common topics of interest" - + " and balance games corresponding to the topics of interest in the form of sentences based on" - + " the interests. At this time, only two choices for the balance game must be given, and the" - + " choices must be separated by a comma The query results must be returned in JSON format" - + " according to the form below and other The JSON value must be answered in Korean without" - + " words. " - + "{\\\"topic\\\": Topic of common interest, \\\"balanceQuestion\\\": [Balance game options]}" - ); + private record Message(String role, String content) { + + private static Message balanceGame() { + return new Message("system", + "당신은 사용자의 관심사들을 입력받아 관심사 게임을 응답하는 챗봇입니다.관심사 게임은 \"공통 관심 주제\"와 \"밸런스 게임의 질문 선택지\" 로 이루어져 있습니다. \"밸런스 게임의 질문 선택지\"는 문장형태로 이루어지며 상반된 각각 하나의 질문으로 무조건 2개 응답되어야 합니다. 이때, \"밸런스 게임의 질문 선택지\"는 각각 36자 이하로 생성되어야 합니다. 응답은 다음 JSON 형태로 응답해주세요. {\"topic\": 공통 관심 주제, \"balanceQuestion\": [밸런스 게임의 질문 선택지 2개]} 이때, 부가적인 설명없이 JSON만 응답해야하며, JSON의 VALUE는 모두 한국어로 응답해주세요."); + } + + private static Message story() { + return new Message("system", + "당신은 사용자의 관심사들을 입력받아 관심사 게임을 응답하는 챗봇입니다. 관심사 게임은 \"공통 관심 주제\"와 \"관심 주제와 연관되는 질문\" 로 이루어져 있습니다.이때 \"관심 주제와 연관되는 질문\" 은 최대 76자로 제한합니다. 응답은 다음 JSON 형태로 형태로 응답해주세요. {\"topic\": 공통 관심 주제, \"story\": 관심 주제와 연관되는 질문} 이때, 부가적인 설명없이 JSON만 응답해야하며, JSON의 VALUE는 모두 한국어로 응답해주세요."); } private static Message user(String interests) { diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 0309d335..bccfe594 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -3,6 +3,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.security.service.RedisService; +import net.teumteum.user.domain.BalanceGameType; import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserRepository; @@ -77,7 +78,7 @@ private User getUser(Long userId) { .orElseThrow(() -> new IllegalArgumentException("userId에 해당하는 user를 찾을 수 없습니다. \"" + userId + "\"")); } - public InterestQuestionResponse getInterestQuestionByUserIds(List userIds) { + public InterestQuestionResponse getInterestQuestionByUserIds(List userIds, String type) { var users = userRepository.findAllById(userIds); Assert.isTrue(users.size() >= 2, () -> { @@ -85,7 +86,7 @@ public InterestQuestionResponse getInterestQuestionByUserIds(List userIds) } ); - return interestQuestion.getQuestion(users); + return BalanceGameType.of(type).getInterestQuestionResponse(users, interestQuestion); } private void deleteUser(User user) { diff --git a/src/test/java/net/teumteum/user/infra/GptInterestQuestionTest.java b/src/test/java/net/teumteum/user/infra/GptInterestQuestionTest.java index e9e169ed..b049a97a 100644 --- a/src/test/java/net/teumteum/user/infra/GptInterestQuestionTest.java +++ b/src/test/java/net/teumteum/user/infra/GptInterestQuestionTest.java @@ -3,7 +3,8 @@ import java.util.List; import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.UserFixture; -import net.teumteum.user.domain.response.InterestQuestionResponse; +import net.teumteum.user.domain.response.BalanceQuestionResponse; +import net.teumteum.user.domain.response.StoryQuestionResponse; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -24,17 +25,16 @@ class GptInterestQuestionTest { @Autowired private InterestQuestion interestQuestion; - @Nested - @DisplayName("getQuestion 메소드는") - class GetQuestion_method { + @DisplayName("getBalanceGame 메소드는") + class GetBalanceGame_method { @Test - @DisplayName("user 목록을 받아서, 관심 질문을 반환한다.") + @DisplayName("user 목록을 받아서, 밸런스 게임을 반환한다.") void Return_balance_game_when_receive_user_list() { // given var users = List.of(UserFixture.getDefaultUser(), UserFixture.getDefaultUser()); - var expected = new InterestQuestionResponse( + var expected = new BalanceQuestionResponse( "프로그래머", List.of("프론트엔드", "백엔드") ); @@ -43,7 +43,7 @@ void Return_balance_game_when_receive_user_list() { gptTestServer.enqueue(expected); // when - var result = interestQuestion.getQuestion(users); + var result = interestQuestion.getBalanceGame(users); // then Assertions.assertThat(expected).isEqualTo(result); @@ -54,7 +54,7 @@ void Return_balance_game_when_receive_user_list() { void Do_retry_when_gpt_server_cannot_receive_interests_lists() { // given var users = List.of(UserFixture.getDefaultUser(), UserFixture.getDefaultUser()); - var expected = new InterestQuestionResponse( + var expected = new BalanceQuestionResponse( "프로그래머", List.of("프론트엔드", "백엔드") ); @@ -66,7 +66,7 @@ void Do_retry_when_gpt_server_cannot_receive_interests_lists() { gptTestServer.enqueue(expected); // when - var result = interestQuestion.getQuestion(users); + var result = interestQuestion.getBalanceGame(users); // then Assertions.assertThat(expected).isEqualTo(result); @@ -84,10 +84,36 @@ void Throw_illegal_state_exception_exceed_5_time_to_get_common_interests() { gptTestServer.enqueue400(); // when - var result = Assertions.catchException(() -> interestQuestion.getQuestion(users)); + var result = Assertions.catchException(() -> interestQuestion.getBalanceGame(users)); // then Assertions.assertThat(result.getClass()).isEqualTo(IllegalStateException.class); } } + + @Nested + @DisplayName("getStoryGame 메소드는") + class GetStoryGame_method { + + @Test + @DisplayName("user 목록을 받아서, 밸런스 게임을 반환한다.") + void Return_story_game_when_receive_user_list() { + // given + var users = List.of(UserFixture.getDefaultUser(), UserFixture.getDefaultUser()); + var expected = new StoryQuestionResponse( + "프로그래머", + "어떤 프로그래머가 좋은 프로그래머 일까요?" + ); + + gptTestServer.enqueue400(); + gptTestServer.enqueue400(); + gptTestServer.enqueue(expected); + + // when + var result = interestQuestion.getStoryGame(users); + + // then + Assertions.assertThat(result).isEqualTo(expected); + } + } }