From adb34797816b5ef9e4d8117d0f9883afd2217a21 Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 26 Aug 2025 04:24:13 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=ED=81=B4=EB=A1=9C=EB=93=9C=20mcp-c?= =?UTF-8?q?lient=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gss-client/gss-mcp-client/build.gradle | 2 ++ .../client/claude/ClaudePrAnaysisClient.java | 13 +++++++++++++ .../OpenAiPrAnalysisClient.java} | 5 +++-- .../devoops/client/{ => openai}/PromptBuilder.java | 2 +- .../src/main/resources/application-mcp-client.yml | 2 ++ ...mplTest.java => OpenAiPrAnalysisClientTest.java} | 11 +++-------- 6 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java rename gss-client/gss-mcp-client/src/main/java/com/devoops/client/{PrAnalysisClientImpl.java => openai/OpenAiPrAnalysisClient.java} (94%) rename gss-client/gss-mcp-client/src/main/java/com/devoops/client/{ => openai}/PromptBuilder.java (96%) rename gss-mcp-app/src/test/java/com/devoops/{PrAnalysisClientImplTest.java => OpenAiPrAnalysisClientTest.java} (85%) diff --git a/gss-client/gss-mcp-client/build.gradle b/gss-client/gss-mcp-client/build.gradle index 4870f73..9565d90 100644 --- a/gss-client/gss-mcp-client/build.gradle +++ b/gss-client/gss-mcp-client/build.gradle @@ -8,4 +8,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6' + implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter' + } diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java new file mode 100644 index 0000000..c349ae3 --- /dev/null +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java @@ -0,0 +1,13 @@ +package com.devoops.client.claude; + +import com.devoops.client.PrAnalysisClient; +import com.devoops.dto.request.AnalyzePrRequest; +import com.devoops.dto.response.AnalyzePrResponse; + +public class ClaudePrAnaysisClient implements PrAnalysisClient { + + @Override + public AnalyzePrResponse analyze(AnalyzePrRequest request) { + return null; + } +} diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClientImpl.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java similarity index 94% rename from gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClientImpl.java rename to gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java index 0f18fba..6ec1f55 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClientImpl.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java @@ -1,5 +1,6 @@ -package com.devoops.client; +package com.devoops.client.openai; +import com.devoops.client.PrAnalysisClient; import com.devoops.dto.request.AnalyzePrRequest; import com.devoops.dto.response.AnalyzePrResponse; import com.devoops.dto.response.PrAnalysis; @@ -19,7 +20,7 @@ @Component @RequiredArgsConstructor @Slf4j -public class PrAnalysisClientImpl implements PrAnalysisClient { +public class OpenAiPrAnalysisClient implements PrAnalysisClient { private final ChatClient chatClient; private final PromptBuilder promptBuilder; diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PromptBuilder.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/PromptBuilder.java similarity index 96% rename from gss-client/gss-mcp-client/src/main/java/com/devoops/client/PromptBuilder.java rename to gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/PromptBuilder.java index 148c9e4..2727876 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PromptBuilder.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/PromptBuilder.java @@ -1,4 +1,4 @@ -package com.devoops.client; +package com.devoops.client.openai; import java.nio.charset.StandardCharsets; import java.util.Base64; diff --git a/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml b/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml index ca3018c..c1d13b9 100644 --- a/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml +++ b/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml @@ -35,6 +35,8 @@ spring: ai: openai: api-key: testKey + anthropic: + api-key: testKey2 --- diff --git a/gss-mcp-app/src/test/java/com/devoops/PrAnalysisClientImplTest.java b/gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java similarity index 85% rename from gss-mcp-app/src/test/java/com/devoops/PrAnalysisClientImplTest.java rename to gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java index ffb935d..334ef2b 100644 --- a/gss-mcp-app/src/test/java/com/devoops/PrAnalysisClientImplTest.java +++ b/gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java @@ -1,13 +1,8 @@ package com.devoops; -import com.devoops.client.PrAnalysisClientImpl; +import com.devoops.client.openai.OpenAiPrAnalysisClient; import com.devoops.dto.request.AnalyzePrRequest; import com.devoops.dto.response.PrAnalysis; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,10 +10,10 @@ @SpringBootTest @Disabled -class PrAnalysisClientImplTest { +class OpenAiPrAnalysisClientTest { @Autowired - PrAnalysisClientImpl prAnalysisClient; + OpenAiPrAnalysisClient prAnalysisClient; @Test void analyzePr_shouldReturnSummaryAndQuestions() { From ea1a77161cdbf1ca39436e41aaf18c12c1531ff2 Mon Sep 17 00:00:00 2001 From: coli Date: Wed, 27 Aug 2025 21:26:31 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=ED=81=B4=EB=A1=9C=EB=93=9C=20mcp-c?= =?UTF-8?q?lient=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gss-client/gss-mcp-client/build.gradle | 3 +- .../client/{openai => }/PromptBuilder.java | 9 +- .../client/claude/ClaudePrAnalysisClient.java | 84 +++++++++++++++++++ .../client/claude/ClaudePrAnaysisClient.java | 13 --- .../client/openai/OpenAiPrAnalysisClient.java | 16 +++- .../java/com/devoops/config/AiConfig.java | 15 ---- .../com/devoops/serdes/PrAnalysisMapper.java | 2 +- .../main/resources/application-mcp-client.yml | 6 ++ .../devoops/OpenAiPrAnalysisClientTest.java | 41 ++++++++- .../src/test/resources/application.yml | 9 ++ 10 files changed, 161 insertions(+), 37 deletions(-) rename gss-client/gss-mcp-client/src/main/java/com/devoops/client/{openai => }/PromptBuilder.java (75%) create mode 100644 gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java delete mode 100644 gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java delete mode 100644 gss-client/gss-mcp-client/src/main/java/com/devoops/config/AiConfig.java diff --git a/gss-client/gss-mcp-client/build.gradle b/gss-client/gss-mcp-client/build.gradle index 9565d90..033157a 100644 --- a/gss-client/gss-mcp-client/build.gradle +++ b/gss-client/gss-mcp-client/build.gradle @@ -8,6 +8,5 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6' - implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter' - + implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter:1.0.0-M6' } diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/PromptBuilder.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PromptBuilder.java similarity index 75% rename from gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/PromptBuilder.java rename to gss-client/gss-mcp-client/src/main/java/com/devoops/client/PromptBuilder.java index 2727876..7a0db16 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/PromptBuilder.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PromptBuilder.java @@ -1,4 +1,4 @@ -package com.devoops.client.openai; +package com.devoops.client; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -14,6 +14,9 @@ public class PromptBuilder { @Value("${dev-oops.github-pr-analysis.system}") private String systemPrompt; + @Value("${dev-oops.github-pr-analysis.format-message}") + private String formatMessage; + public String buildUserPrompt(String title, String description, String diff) { return promptTemplate .replace("{title}", title) @@ -25,6 +28,10 @@ public String buildSystemPrompt() { return systemPrompt; } + public String buildSystemPromptWithResponseFormat(String jsonSchema) { + return systemPrompt + "\n\n" + formatMessage.formatted(jsonSchema); + } + private String encodeDiff(String diff) { return Base64.getEncoder().encodeToString(diff.getBytes(StandardCharsets.UTF_8)); } diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java new file mode 100644 index 0000000..dc7b7ff --- /dev/null +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java @@ -0,0 +1,84 @@ +package com.devoops.client.claude; + +import com.devoops.client.PrAnalysisClient; +import com.devoops.client.PromptBuilder; +import com.devoops.dto.request.AnalyzePrRequest; +import com.devoops.dto.response.AnalyzePrResponse; +import com.devoops.dto.response.PrAnalysis; +import com.devoops.serdes.PrAnalysisMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.anthropic.AnthropicChatModel; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.metadata.Usage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ClaudePrAnalysisClient implements PrAnalysisClient { + + private final ChatClient chatClient; + private final PromptBuilder promptBuilder; + private final PrAnalysisMapper prAnalysisMapper; + + public ClaudePrAnalysisClient( + AnthropicChatModel anthropicChatModel, + PromptBuilder promptBuilder, + PrAnalysisMapper prAnalysisMapper + ) { + this.chatClient = ChatClient.create(anthropicChatModel); + this.promptBuilder = promptBuilder; + this.prAnalysisMapper = prAnalysisMapper; + } + + @Override + public AnalyzePrResponse analyze(AnalyzePrRequest request) { + //option 설정 + ChatOptions anthropicChatOptions = anthropicChatOptions(request.model()); + + ChatResponse chatresponse = callChatResponse( + request.title(), + request.description(), + request.codeDifference(), + anthropicChatOptions + ); + //claude-sonnet-4-20250514 + Usage usage = chatresponse.getMetadata().getUsage(); + String analysisResult = chatresponse.getResult().getOutput().getText(); + PrAnalysis prAnalysis = prAnalysisMapper.mapToPrAnalysis(analysisResult); + return new AnalyzePrResponse(usage, prAnalysis); + } + + private ChatOptions anthropicChatOptions(String model) { + //"claude-sonnet-4-20250514" + return ChatOptions.builder() + .temperature(0.7) + .model(model) + .maxTokens(64_000) + .build(); + } + + private ChatResponse callChatResponse( + String title, + String description, + String codeDifference, + ChatOptions options + ) { + String userPrompt = promptBuilder.buildUserPrompt(title, description, codeDifference); + String systemPrompt = promptBuilder.buildSystemPromptWithResponseFormat(outputJsonSchema()); + log.info("systemPrompt: {}", systemPrompt); + return chatClient.prompt() + .options(options) + .system(systemPrompt) + .user(userPrompt) + .call() + .chatResponse(); + } + + private String outputJsonSchema() { + BeanOutputConverter outputConverter = new BeanOutputConverter<>(PrAnalysis.class); + return outputConverter.getJsonSchema(); + } +} diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java deleted file mode 100644 index c349ae3..0000000 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnaysisClient.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.devoops.client.claude; - -import com.devoops.client.PrAnalysisClient; -import com.devoops.dto.request.AnalyzePrRequest; -import com.devoops.dto.response.AnalyzePrResponse; - -public class ClaudePrAnaysisClient implements PrAnalysisClient { - - @Override - public AnalyzePrResponse analyze(AnalyzePrRequest request) { - return null; - } -} diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java index 6ec1f55..8358b0b 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java @@ -1,24 +1,26 @@ package com.devoops.client.openai; import com.devoops.client.PrAnalysisClient; +import com.devoops.client.PromptBuilder; import com.devoops.dto.request.AnalyzePrRequest; import com.devoops.dto.response.AnalyzePrResponse; import com.devoops.dto.response.PrAnalysis; import com.devoops.serdes.PrAnalysisMapper; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.metadata.Usage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.ResponseFormat; import org.springframework.ai.openai.api.ResponseFormat.Type; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; +@Primary @Component -@RequiredArgsConstructor @Slf4j public class OpenAiPrAnalysisClient implements PrAnalysisClient { @@ -26,6 +28,16 @@ public class OpenAiPrAnalysisClient implements PrAnalysisClient { private final PromptBuilder promptBuilder; private final PrAnalysisMapper prAnalysisMapper; + public OpenAiPrAnalysisClient( + OpenAiChatModel openAiChatModel, + PromptBuilder promptBuilder, + PrAnalysisMapper prAnalysisMapper + ) { + this.chatClient = ChatClient.create(openAiChatModel); + this.promptBuilder = promptBuilder; + this.prAnalysisMapper = prAnalysisMapper; + } + @Override public AnalyzePrResponse analyze(AnalyzePrRequest request) { //option 설정 diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/config/AiConfig.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/config/AiConfig.java deleted file mode 100644 index 6fc1e02..0000000 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/config/AiConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.devoops.config; - -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AiConfig { - - @Bean - public ChatClient chatClient(OpenAiChatModel chatModel) { - return ChatClient.create(chatModel); - } -} diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/serdes/PrAnalysisMapper.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/serdes/PrAnalysisMapper.java index 19af851..96903df 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/serdes/PrAnalysisMapper.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/serdes/PrAnalysisMapper.java @@ -12,7 +12,7 @@ @Component public class PrAnalysisMapper { - private static final int MAX_LOGGING_LENGTH = 255; + private static final int MAX_LOGGING_LENGTH = 500; private static final ObjectMapper MAPPER = new ObjectMapper() .findAndRegisterModules() diff --git a/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml b/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml index c1d13b9..5fac40d 100644 --- a/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml +++ b/gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml @@ -26,6 +26,12 @@ dev-oops: diff를 Base64에서 디코딩한 후 분석하고 PR 요약과 질문을 만들어 주세요. + format-message: | + 응답은 반드시 다음 JSON 스키마에 맞춰 정확한 JSON 형식으로 제공해주세요('''json도 제외):: + + %s + + 응답에는 JSON만 포함하고, 다른 텍스트나 설명은 포함하지 마세요. --- spring: diff --git a/gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java b/gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java index 334ef2b..4798d36 100644 --- a/gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java +++ b/gss-mcp-app/src/test/java/com/devoops/OpenAiPrAnalysisClientTest.java @@ -1,5 +1,6 @@ package com.devoops; +import com.devoops.client.claude.ClaudePrAnalysisClient; import com.devoops.client.openai.OpenAiPrAnalysisClient; import com.devoops.dto.request.AnalyzePrRequest; import com.devoops.dto.response.PrAnalysis; @@ -13,10 +14,13 @@ class OpenAiPrAnalysisClientTest { @Autowired - OpenAiPrAnalysisClient prAnalysisClient; + private OpenAiPrAnalysisClient openAiPrAnalysisClient; + + @Autowired + private ClaudePrAnalysisClient claudePrAnalysisClient; @Test - void analyzePr_shouldReturnSummaryAndQuestions() { + void analyzePr_shouldReturnSummaryAndQuestions_openai() { // given String title = "회원가입 시 이메일 중복 체크 로직 추가"; String desc = "회원가입 시 이메일 중복 체크 로직 추가입니다."; @@ -36,7 +40,38 @@ void analyzePr_shouldReturnSummaryAndQuestions() { long startTime = System.currentTimeMillis(); AnalyzePrRequest request = new AnalyzePrRequest(title, desc, diff, "gpt-5-nano"); - PrAnalysis result = prAnalysisClient.analyze(request).prAnalysis(); + PrAnalysis result = openAiPrAnalysisClient.analyze(request).prAnalysis(); + long endTime = System.currentTimeMillis(); + + System.out.println(endTime - startTime+ "ms"); + + System.out.println("📝 요약: " + result.summary()); + result.summaryDetails().forEach(q -> System.out.println("- " + q)); + result.questions().forEach(q -> System.out.println("- " + q)); + } + + @Test + void analyzePr_shouldReturnSummaryAndQuestions_claude() { + // given + String title = "회원가입 시 이메일 중복 체크 로직 추가"; + String desc = "회원가입 시 이메일 중복 체크 로직 추가입니다."; + String diff = """ + diff --git a/UserService.java b/UserService.java + + public boolean isEmailTaken(String email) { + + return userRepository.existsByEmail(email); + + } + + + + public void register(User user) { + + if (isEmailTaken(user.getEmail())) { + + throw new DuplicateEmailException(); + + } + + userRepository.save(user); + + } + """; + + long startTime = System.currentTimeMillis(); + AnalyzePrRequest request = new AnalyzePrRequest(title, desc, diff, "claude-sonnet-4-20250514"); + PrAnalysis result = claudePrAnalysisClient.analyze(request).prAnalysis(); long endTime = System.currentTimeMillis(); System.out.println(endTime - startTime+ "ms"); diff --git a/gss-mcp-app/src/test/resources/application.yml b/gss-mcp-app/src/test/resources/application.yml index 1b1a652..99993be 100644 --- a/gss-mcp-app/src/test/resources/application.yml +++ b/gss-mcp-app/src/test/resources/application.yml @@ -30,6 +30,8 @@ spring: ai: openai: api-key: test + anthropic: + api-key: test dev-oops: mcp: @@ -60,3 +62,10 @@ dev-oops: --- END DIFF --- diff를 Base64에서 디코딩한 후 분석하고 PR 요약과 질문을 만들어 주세요. + + format-message: | + 응답은 반드시 다음 JSON 스키마에 맞춰 정확한 JSON 형식으로 제공해주세요('''json도 제외): + + %s + + 응답에는 JSON만 포함하고, 다른 텍스트나 설명은 포함하지 마세요. From 4406ae01c7466c8ec7b857ad48e232a16fbf1bb7 Mon Sep 17 00:00:00 2001 From: coli Date: Wed, 27 Aug 2025 21:53:21 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20api=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/github/repo/GithubRepoDomainRepository.java | 2 -- .../github/repo/GithubRepoDomainRepositoryImpl.java | 6 ------ .../jpa/repository/github/repo/GithubRepoJpaRepository.java | 2 -- 3 files changed, 10 deletions(-) diff --git a/gss-domain/src/main/java/com/devoops/domain/repository/github/repo/GithubRepoDomainRepository.java b/gss-domain/src/main/java/com/devoops/domain/repository/github/repo/GithubRepoDomainRepository.java index f198df6..b4138e1 100644 --- a/gss-domain/src/main/java/com/devoops/domain/repository/github/repo/GithubRepoDomainRepository.java +++ b/gss-domain/src/main/java/com/devoops/domain/repository/github/repo/GithubRepoDomainRepository.java @@ -17,8 +17,6 @@ public interface GithubRepoDomainRepository { List findByUserId(long userId); - boolean existsByExternalIdAndUserId(long externalId, long userId); - Optional findByExternalIdAndUserId(long externalId, long userId); GithubRepository getByExternalIdAndUserId(long externalId, long userId); diff --git a/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoDomainRepositoryImpl.java b/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoDomainRepositoryImpl.java index 22ea104..bfeefc6 100644 --- a/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoDomainRepositoryImpl.java +++ b/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoDomainRepositoryImpl.java @@ -41,12 +41,6 @@ public boolean existsByIdAndUserId(long id, long userId) { return repoJpaRepository.existsByIdAndUserId(id, userId); } - @Override - @Transactional(readOnly = true) - public boolean existsByExternalIdAndUserId(long externalId, long userId) { - return repoJpaRepository.existsByGithubRepositoryIdAndUserId(externalId, userId); - } - @Override @Transactional(readOnly = true) public GithubRepository findByIdAndUserId(long id, long userId) { diff --git a/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoJpaRepository.java b/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoJpaRepository.java index f7cc064..c726ced 100644 --- a/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoJpaRepository.java +++ b/gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoJpaRepository.java @@ -15,7 +15,5 @@ public interface GithubRepoJpaRepository extends JpaRepository findByGithubRepositoryId(long externalId); - Optional findByGithubRepositoryIdAndUserId(long externalId, long userId); } From a2c5f1d6be8756837a87ddd7a19c506a659714a9 Mon Sep 17 00:00:00 2001 From: coli Date: Wed, 27 Aug 2025 23:30:18 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20client=20=EB=B9=88=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=88=9C=EC=84=9C=20=EA=B2=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/claude/ClaudePrAnalysisClient.java | 4 --- .../java/com/devoops/config/ClientConfig.java | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java index dc7b7ff..a76aae2 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java @@ -13,10 +13,8 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.converter.BeanOutputConverter; -import org.springframework.stereotype.Component; @Slf4j -@Component public class ClaudePrAnalysisClient implements PrAnalysisClient { private final ChatClient chatClient; @@ -44,7 +42,6 @@ public AnalyzePrResponse analyze(AnalyzePrRequest request) { request.codeDifference(), anthropicChatOptions ); - //claude-sonnet-4-20250514 Usage usage = chatresponse.getMetadata().getUsage(); String analysisResult = chatresponse.getResult().getOutput().getText(); PrAnalysis prAnalysis = prAnalysisMapper.mapToPrAnalysis(analysisResult); @@ -68,7 +65,6 @@ private ChatResponse callChatResponse( ) { String userPrompt = promptBuilder.buildUserPrompt(title, description, codeDifference); String systemPrompt = promptBuilder.buildSystemPromptWithResponseFormat(outputJsonSchema()); - log.info("systemPrompt: {}", systemPrompt); return chatClient.prompt() .options(options) .system(systemPrompt) diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java new file mode 100644 index 0000000..8d130af --- /dev/null +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java @@ -0,0 +1,36 @@ +package com.devoops.config; + +import com.devoops.client.PrAnalysisClient; +import com.devoops.client.PromptBuilder; +import com.devoops.client.claude.ClaudePrAnalysisClient; +import com.devoops.client.openai.OpenAiPrAnalysisClient; +import com.devoops.serdes.PrAnalysisMapper; +import org.springframework.ai.anthropic.AnthropicChatModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +@Configuration +public class ClientConfig { + + @Bean + @Order(1) + public PrAnalysisClient openAiClient( + OpenAiChatModel openAiChatModel, + PromptBuilder promptBuilder, + PrAnalysisMapper prAnalysisMapper + ) { + return new OpenAiPrAnalysisClient(openAiChatModel, promptBuilder, prAnalysisMapper); + } + + @Bean + @Order(2) + public PrAnalysisClient claudeClient( + AnthropicChatModel anthropicChatModel, + PromptBuilder promptBuilder, + PrAnalysisMapper prAnalysisMapper + ) { + return new ClaudePrAnalysisClient(anthropicChatModel, promptBuilder, prAnalysisMapper); + } +} From defdf0fb528eff3df995e39e0d8f3f5de4c587ee Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 28 Aug 2025 00:45:27 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20aiModel=EC=9D=84=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/entity/analysis/AiModel.java | 8 +++++++ .../domain/entity/analysis/ClaudeAiModel.java | 23 +++++++++++++++++++ .../domain/entity/analysis/OpenAiModel.java | 13 ++++++++--- .../com/devoops/adaptor/AiModelSelector.java | 19 +++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 gss-domain/src/main/java/com/devoops/domain/entity/analysis/AiModel.java create mode 100644 gss-domain/src/main/java/com/devoops/domain/entity/analysis/ClaudeAiModel.java create mode 100644 gss-mcp-app/src/main/java/com/devoops/adaptor/AiModelSelector.java diff --git a/gss-domain/src/main/java/com/devoops/domain/entity/analysis/AiModel.java b/gss-domain/src/main/java/com/devoops/domain/entity/analysis/AiModel.java new file mode 100644 index 0000000..aabec21 --- /dev/null +++ b/gss-domain/src/main/java/com/devoops/domain/entity/analysis/AiModel.java @@ -0,0 +1,8 @@ +package com.devoops.domain.entity.analysis; + +public interface AiModel { + + double getCharge(int promptToken, int completionTokens); + + String getName(); +} diff --git a/gss-domain/src/main/java/com/devoops/domain/entity/analysis/ClaudeAiModel.java b/gss-domain/src/main/java/com/devoops/domain/entity/analysis/ClaudeAiModel.java new file mode 100644 index 0000000..d203ee2 --- /dev/null +++ b/gss-domain/src/main/java/com/devoops/domain/entity/analysis/ClaudeAiModel.java @@ -0,0 +1,23 @@ +package com.devoops.domain.entity.analysis; + +import com.devoops.util.CurrencyUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ClaudeAiModel implements AiModel { + + CLAUDE_SONNET_4("claude-sonnet-4-20250514", 0.000003, 0.000015), + ; + + private final String name; + private final double inputTokenCharge; //달러 + private final double outputTokenCharge; //달러 + + public double getCharge(int promptToken, int completionTokens) { + double inputCharge = CurrencyUtil.usdToKrw(inputTokenCharge * promptToken); + double outputCharge = CurrencyUtil.usdToKrw(outputTokenCharge * completionTokens); + return inputCharge + outputCharge; + } +} diff --git a/gss-domain/src/main/java/com/devoops/domain/entity/analysis/OpenAiModel.java b/gss-domain/src/main/java/com/devoops/domain/entity/analysis/OpenAiModel.java index b316425..12b4cf4 100644 --- a/gss-domain/src/main/java/com/devoops/domain/entity/analysis/OpenAiModel.java +++ b/gss-domain/src/main/java/com/devoops/domain/entity/analysis/OpenAiModel.java @@ -8,7 +8,7 @@ @Getter @RequiredArgsConstructor -public enum OpenAiModel { +public enum OpenAiModel implements AiModel { GPT_5(0, 7500, "gpt-5", 0.00000125, 0.00001), GPT_5_MINI(7501, 12500, "gpt-5-mini", 0.00000025, 0.000002), @@ -23,12 +23,19 @@ public enum OpenAiModel { public static OpenAiModel getModelByUsage(BigDecimal currentUsageWon) { return Stream.of(values()) - .filter(model -> model.moneyUnderCriteria <= currentUsageWon.doubleValue() - && model.moneyUpperCriteria >= currentUsageWon.doubleValue()) + .filter(model -> model.isBetween( + currentUsageWon.doubleValue(), + model.moneyUnderCriteria, + model.moneyUpperCriteria) + ) .findAny() .orElse(GPT_5_NANO); } + private boolean isBetween(double currentUsage, double min, double max) { + return currentUsage >= min && currentUsage <= max; + } + public double getCharge(int promptToken, int completionTokens) { double inputCharge = CurrencyUtil.usdToKrw(inputTokenCharge * promptToken); double outputCharge = CurrencyUtil.usdToKrw(outputTokenCharge * completionTokens); diff --git a/gss-mcp-app/src/main/java/com/devoops/adaptor/AiModelSelector.java b/gss-mcp-app/src/main/java/com/devoops/adaptor/AiModelSelector.java new file mode 100644 index 0000000..873a298 --- /dev/null +++ b/gss-mcp-app/src/main/java/com/devoops/adaptor/AiModelSelector.java @@ -0,0 +1,19 @@ +package com.devoops.adaptor; + +import com.devoops.McpClientType; +import com.devoops.domain.entity.analysis.AiModel; +import com.devoops.domain.entity.analysis.ClaudeAiModel; +import com.devoops.domain.entity.analysis.OpenAiModel; +import java.math.BigDecimal; +import org.springframework.stereotype.Component; + +@Component +public class AiModelSelector { + + public AiModel getAiModel(McpClientType clientType, BigDecimal aiCharge) { + if(clientType.isOpenAi()) { + return OpenAiModel.getModelByUsage(aiCharge); + } + return ClaudeAiModel.CLAUDE_SONNET_4; + } +} From e17c1b5a0e52fedb0bb418c0c6d823dd14a57d73 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 28 Aug 2025 00:46:35 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20openai=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=8B=9C=20claude=EB=A5=BC=20=ED=98=B8=EC=B6=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/devoops/McpClientType.java | 12 ++++ .../com/devoops/client/PrAnalysisClient.java | 2 + .../client/claude/ClaudePrAnalysisClient.java | 9 ++- .../client/openai/OpenAiPrAnalysisClient.java | 8 +++ .../java/com/devoops/config/ClientConfig.java | 2 + .../exception/errorcode/ErrorCode.java | 1 + .../devoops/adaptor/PrAnalysisAdapter.java | 29 ++++++-- .../service/pranalysis/PrAnalysisService.java | 2 +- .../adaptor/PrAnalysisAdapterTest.java | 69 +++++++++++++++++++ 9 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 gss-client/gss-mcp-client/src/main/java/com/devoops/McpClientType.java create mode 100644 gss-mcp-app/src/test/java/com/devoops/adaptor/PrAnalysisAdapterTest.java diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/McpClientType.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/McpClientType.java new file mode 100644 index 0000000..80bf90e --- /dev/null +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/McpClientType.java @@ -0,0 +1,12 @@ +package com.devoops; + +public enum McpClientType { + + OPEN_AI, + CLAUDE, + ; + + public boolean isOpenAi() { + return this == OPEN_AI; + } +} diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClient.java index f587f86..0602ab2 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClient.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/PrAnalysisClient.java @@ -1,5 +1,6 @@ package com.devoops.client; +import com.devoops.McpClientType; import com.devoops.dto.request.AnalyzePrRequest; import com.devoops.dto.response.AnalyzePrResponse; @@ -7,4 +8,5 @@ public interface PrAnalysisClient { AnalyzePrResponse analyze(AnalyzePrRequest request); + McpClientType getMcpClientType(); } diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java index a76aae2..7e68420 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/claude/ClaudePrAnalysisClient.java @@ -1,5 +1,6 @@ package com.devoops.client.claude; +import com.devoops.McpClientType; import com.devoops.client.PrAnalysisClient; import com.devoops.client.PromptBuilder; import com.devoops.dto.request.AnalyzePrRequest; @@ -17,6 +18,8 @@ @Slf4j public class ClaudePrAnalysisClient implements PrAnalysisClient { + private static final McpClientType CLIENT_VENDOR = McpClientType.CLAUDE; + private final ChatClient chatClient; private final PromptBuilder promptBuilder; private final PrAnalysisMapper prAnalysisMapper; @@ -48,8 +51,12 @@ public AnalyzePrResponse analyze(AnalyzePrRequest request) { return new AnalyzePrResponse(usage, prAnalysis); } + @Override + public McpClientType getMcpClientType() { + return CLIENT_VENDOR; + } + private ChatOptions anthropicChatOptions(String model) { - //"claude-sonnet-4-20250514" return ChatOptions.builder() .temperature(0.7) .model(model) diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java index 8358b0b..3c942bd 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/client/openai/OpenAiPrAnalysisClient.java @@ -1,5 +1,6 @@ package com.devoops.client.openai; +import com.devoops.McpClientType; import com.devoops.client.PrAnalysisClient; import com.devoops.client.PromptBuilder; import com.devoops.dto.request.AnalyzePrRequest; @@ -24,6 +25,8 @@ @Slf4j public class OpenAiPrAnalysisClient implements PrAnalysisClient { + private static final McpClientType MCP_CLIENT_VENDOR = McpClientType.OPEN_AI; + private final ChatClient chatClient; private final PromptBuilder promptBuilder; private final PrAnalysisMapper prAnalysisMapper; @@ -58,6 +61,11 @@ public AnalyzePrResponse analyze(AnalyzePrRequest request) { return new AnalyzePrResponse(usage, prAnalysis); } + @Override + public McpClientType getMcpClientType() { + return MCP_CLIENT_VENDOR; + } + private OpenAiChatOptions.Builder openAiChatBuilder() { return OpenAiChatOptions.builder() .responseFormat(new ResponseFormat(Type.JSON_SCHEMA, outputJsonSchema())) diff --git a/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java b/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java index 8d130af..276c899 100644 --- a/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java +++ b/gss-client/gss-mcp-client/src/main/java/com/devoops/config/ClientConfig.java @@ -9,8 +9,10 @@ import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.core.annotation.Order; +@Profile("!test") @Configuration public class ClientConfig { diff --git a/gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java b/gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java index 75740fa..ce93fb5 100644 --- a/gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java +++ b/gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java @@ -40,6 +40,7 @@ public enum ErrorCode { REDIS_SUBSCRIBE_ERROR(500, "레디스 이벤트 수신 과정에서 문제가 생겼습니다"), GITHUB_CLIENT_ERROR(500, "깃허브 클라이언트 소통과정에 문제가 발생했습니다"), AI_RESPONSE_PARSING_ERROR(500, "AI로부터 온 질문 생성을 파싱하는 과정에 오류가 발생했습니다"), + AI_CREATE_QUESTION_ERROR(500, "AI 질문 생성과정에 오류가 발생했습니다") ; private final int statusCode; diff --git a/gss-mcp-app/src/main/java/com/devoops/adaptor/PrAnalysisAdapter.java b/gss-mcp-app/src/main/java/com/devoops/adaptor/PrAnalysisAdapter.java index a2a612a..0ae0c1a 100644 --- a/gss-mcp-app/src/main/java/com/devoops/adaptor/PrAnalysisAdapter.java +++ b/gss-mcp-app/src/main/java/com/devoops/adaptor/PrAnalysisAdapter.java @@ -1,21 +1,40 @@ package com.devoops.adaptor; +import com.devoops.McpClientType; import com.devoops.client.PrAnalysisClient; +import com.devoops.domain.entity.analysis.AiModel; +import com.devoops.domain.entity.analysis.OpenAiModel; import com.devoops.dto.request.AdaptedAnalyzePrResponse; import com.devoops.dto.request.AnalyzePrRequest; import com.devoops.dto.response.AnalyzePrResponse; +import com.devoops.exception.custom.GssException; +import com.devoops.exception.errorcode.ErrorCode; +import java.math.BigDecimal; +import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class PrAnalysisAdapter { - private final PrAnalysisClient prAnalysisClient; + private final List prAnalysisClients; + private final AiModelSelector aiModelSelector; - public AdaptedAnalyzePrResponse analyze(String title, String description, String diff, String model) { - AnalyzePrRequest analyzePrRequest = new AnalyzePrRequest(title, description, diff, model); - AnalyzePrResponse analyzePrResponse = prAnalysisClient.analyze(analyzePrRequest); - return new AdaptedAnalyzePrResponse(analyzePrResponse); + public AdaptedAnalyzePrResponse analyze(String title, String description, String diff, BigDecimal aiCharge) { + for (PrAnalysisClient prAnalysisClient : prAnalysisClients) { + try { + McpClientType mcpClientType = prAnalysisClient.getMcpClientType(); + AiModel aiModel = aiModelSelector.getAiModel(mcpClientType, aiCharge); + AnalyzePrRequest analyzePrRequest = new AnalyzePrRequest(title, description, diff, aiModel.getName()); + AnalyzePrResponse analyzePrResponse = prAnalysisClient.analyze(analyzePrRequest); + return new AdaptedAnalyzePrResponse(analyzePrResponse); + } catch (Exception e) { + log.error("client type {} 질문 생성 중 오류 발생 : {}", prAnalysisClient.getMcpClientType(), e.getMessage());; + } + } + throw new GssException(ErrorCode.AI_CREATE_QUESTION_ERROR); } } diff --git a/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisService.java b/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisService.java index e6e9aa5..c1673e6 100644 --- a/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisService.java +++ b/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisService.java @@ -32,7 +32,7 @@ public AdaptedAnalyzePrResponse analyzePullRequest(AppWebhookEventRequest reques request.title(), request.description(), diff, - aiModel.getName() + aiCharge.getCharge() ); double consumedCharge = aiModel.getCharge(result.promptTokens(), result.completionTokens()); diff --git a/gss-mcp-app/src/test/java/com/devoops/adaptor/PrAnalysisAdapterTest.java b/gss-mcp-app/src/test/java/com/devoops/adaptor/PrAnalysisAdapterTest.java new file mode 100644 index 0000000..6b01366 --- /dev/null +++ b/gss-mcp-app/src/test/java/com/devoops/adaptor/PrAnalysisAdapterTest.java @@ -0,0 +1,69 @@ +package com.devoops.adaptor; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import com.devoops.BaseMcpTest; +import com.devoops.dto.request.AnalyzePrRequest; +import com.devoops.exception.custom.GssException; +import com.devoops.exception.errorcode.ErrorCode; +import java.math.BigDecimal; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +class PrAnalysisAdapterTest extends BaseMcpTest { + + @Autowired + private PrAnalysisAdapter prAnalysisAdapter; + + @Nested + class SelectClient { + + @Test + void openAi에게_최초_호출한다() { + prAnalysisAdapter.analyze("title", "description", "diff", BigDecimal.ZERO); + + assertAll( + () -> Mockito.verify(openAiPrAnalysisClient, times(1)).analyze(any(AnalyzePrRequest.class)), + () -> Mockito.verify(claudePrAnalysisClient, never()).analyze(any(AnalyzePrRequest.class)) + ); + } + + @Test + void openAi가_실패하면_claude에게_질문_생성을_호출한다() { + Mockito.when(openAiPrAnalysisClient.analyze(any(AnalyzePrRequest.class))) + .thenThrow(GssException.class); + + prAnalysisAdapter.analyze("title", "description", "diff", BigDecimal.ZERO); + + assertAll( + () -> Mockito.verify(openAiPrAnalysisClient, times(1)).analyze(any(AnalyzePrRequest.class)), + () -> Mockito.verify(claudePrAnalysisClient, times(1)).analyze(any(AnalyzePrRequest.class)) + ); + } + + @Test + void 모든_클라이언트_호출이_실패하면_에러가_발생한다() { + Mockito.when(openAiPrAnalysisClient.analyze(any(AnalyzePrRequest.class))) + .thenThrow(GssException.class); + + Mockito.when(claudePrAnalysisClient.analyze(any(AnalyzePrRequest.class))) + .thenThrow(GssException.class); + + assertAll( + () -> assertThatThrownBy( + () -> prAnalysisAdapter.analyze("title", "description", "diff", BigDecimal.ZERO) + ) + .isInstanceOf(GssException.class) + .hasMessage(ErrorCode.AI_CREATE_QUESTION_ERROR.getMessage()), + () -> Mockito.verify(openAiPrAnalysisClient, times(1)).analyze(any(AnalyzePrRequest.class)), + () -> Mockito.verify(claudePrAnalysisClient, times(1)).analyze(any(AnalyzePrRequest.class)) + ); + } + } +} From 8ef7891733d475081f3432ee308f57b42db87958 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 28 Aug 2025 00:46:53 +0900 Subject: [PATCH 7/8] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/java/com/devoops/BaseMcpTest.java | 33 +++++++++++- .../java/com/devoops/config/TestConfig.java | 20 ------- .../devoops/fake/FakePrAnalysisClient.java | 53 ------------------- .../devoops/fixture/PrAnalysisFixture.java | 41 ++++++++++++++ 4 files changed, 72 insertions(+), 75 deletions(-) delete mode 100644 gss-mcp-app/src/test/java/com/devoops/config/TestConfig.java delete mode 100644 gss-mcp-app/src/test/java/com/devoops/fake/FakePrAnalysisClient.java create mode 100644 gss-mcp-app/src/test/java/com/devoops/fixture/PrAnalysisFixture.java diff --git a/gss-mcp-app/src/test/java/com/devoops/BaseMcpTest.java b/gss-mcp-app/src/test/java/com/devoops/BaseMcpTest.java index a1dc176..b9447e1 100644 --- a/gss-mcp-app/src/test/java/com/devoops/BaseMcpTest.java +++ b/gss-mcp-app/src/test/java/com/devoops/BaseMcpTest.java @@ -1,20 +1,28 @@ package com.devoops; -import com.devoops.config.TestConfig; +import static org.mockito.ArgumentMatchers.any; + +import com.devoops.client.claude.ClaudePrAnalysisClient; +import com.devoops.client.openai.OpenAiPrAnalysisClient; +import com.devoops.dto.request.AnalyzePrRequest; +import com.devoops.fixture.PrAnalysisFixture; import com.devoops.generator.AnswerGenerator; import com.devoops.generator.AnswerRankingGenerator; import com.devoops.generator.GithubRepoGenerator; import com.devoops.generator.PullRequestGenerator; import com.devoops.generator.QuestionGenerator; import com.devoops.generator.UserGenerator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; @ExtendWith(DataBaseCleaner.class) -@Import({TestConfig.class, RedisTestConfiguration.class}) +@Import({RedisTestConfiguration.class}) @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public abstract class BaseMcpTest { @@ -36,4 +44,25 @@ public abstract class BaseMcpTest { @Autowired protected AnswerRankingGenerator answerRankingGenerator; + + @MockitoBean + protected OpenAiPrAnalysisClient openAiPrAnalysisClient; + + @MockitoBean + protected ClaudePrAnalysisClient claudePrAnalysisClient; + + @BeforeEach + void setUp() { + Mockito.doReturn(PrAnalysisFixture.MOCK_RESPONSE) + .when(openAiPrAnalysisClient).analyze(any(AnalyzePrRequest.class)); + + Mockito.doReturn(McpClientType.OPEN_AI) + .when(openAiPrAnalysisClient).getMcpClientType(); + + Mockito.doReturn(PrAnalysisFixture.MOCK_RESPONSE) + .when(claudePrAnalysisClient).analyze(any(AnalyzePrRequest.class)); + + Mockito.doReturn(McpClientType.CLAUDE) + .when(claudePrAnalysisClient).getMcpClientType(); + } } diff --git a/gss-mcp-app/src/test/java/com/devoops/config/TestConfig.java b/gss-mcp-app/src/test/java/com/devoops/config/TestConfig.java deleted file mode 100644 index 8fc17c5..0000000 --- a/gss-mcp-app/src/test/java/com/devoops/config/TestConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.devoops.config; - - -import com.devoops.client.PrAnalysisClient; -import com.devoops.fake.FakePrAnalysisClient; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; - -@Profile(("test")) -@TestConfiguration -public class TestConfig { - - @Bean - @Primary - public PrAnalysisClient prAnalysisClient() { - return new FakePrAnalysisClient(); - } -} diff --git a/gss-mcp-app/src/test/java/com/devoops/fake/FakePrAnalysisClient.java b/gss-mcp-app/src/test/java/com/devoops/fake/FakePrAnalysisClient.java deleted file mode 100644 index b6b04f6..0000000 --- a/gss-mcp-app/src/test/java/com/devoops/fake/FakePrAnalysisClient.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.devoops.fake; - -import com.devoops.client.PrAnalysisClient; -import com.devoops.dto.request.AnalyzePrRequest; -import com.devoops.dto.response.AnalyzePrResponse; -import com.devoops.dto.response.PrAnalysis; -import java.util.Arrays; -import java.util.List; -import org.springframework.context.annotation.Profile; - -@Profile("test") -public class FakePrAnalysisClient implements PrAnalysisClient { - - public static PrAnalysis MOCK_RESPONSE = new PrAnalysis( - "이 PR은 사용자 인증 로직에 JWT 기반의 토큰 갱신 기능을 추가했습니다.", - List.of( - new PrAnalysis.SummaryDetails( - "JWT 리프레시 토큰 기능 추가", - "기존 로그인 로직에 리프레시 토큰 발급 및 재발급 기능을 추가하여, 사용자 인증 세션을 안전하게 유지할 수 있도록 개선했습니다." - ), - new PrAnalysis.SummaryDetails( - "보안 강화 및 예외 처리 보완", - "토큰 유효성 검사 과정에서 발생할 수 있는 다양한 예외를 핸들링하며, 불필요한 정보 노출을 막기 위한 응답 구조도 정비했습니다." - ) - ), - List.of( - new PrAnalysis.CategorizedQuestion( - "보안", - Arrays.asList( - "JWT 토큰을 사용할 때 고려해야 할 보안 취약점은 무엇인가요?", - "리프레시 토큰 저장 위치와 전달 방식에 따른 장단점은 무엇인가요?" - ) - ), - new PrAnalysis.CategorizedQuestion( - "유지보수성", - Arrays.asList( - "토큰 로직을 추상화하거나 모듈화할 때 고려해야 할 요소는 무엇인가요?", - "예외 처리를 공통화할 때 코드의 가독성과 유지보수성을 어떻게 확보할 수 있나요?" - ) - ) - ) - ); - - @Override - public AnalyzePrResponse analyze(AnalyzePrRequest request) { - return new AnalyzePrResponse( - 100, - 100, - 200, - MOCK_RESPONSE - ); - } -} diff --git a/gss-mcp-app/src/test/java/com/devoops/fixture/PrAnalysisFixture.java b/gss-mcp-app/src/test/java/com/devoops/fixture/PrAnalysisFixture.java new file mode 100644 index 0000000..6812414 --- /dev/null +++ b/gss-mcp-app/src/test/java/com/devoops/fixture/PrAnalysisFixture.java @@ -0,0 +1,41 @@ +package com.devoops.fixture; + +import com.devoops.dto.response.AnalyzePrResponse; +import com.devoops.dto.response.PrAnalysis; +import java.util.Arrays; +import java.util.List; + +public class PrAnalysisFixture { + + public static AnalyzePrResponse MOCK_RESPONSE = new AnalyzePrResponse( + 10, 10, 20, + new PrAnalysis( + "이 PR은 사용자 인증 로직에 JWT 기반의 토큰 갱신 기능을 추가했습니다.", + List.of( + new PrAnalysis.SummaryDetails( + "JWT 리프레시 토큰 기능 추가", + "기존 로그인 로직에 리프레시 토큰 발급 및 재발급 기능을 추가하여, 사용자 인증 세션을 안전하게 유지할 수 있도록 개선했습니다." + ), + new PrAnalysis.SummaryDetails( + "보안 강화 및 예외 처리 보완", + "토큰 유효성 검사 과정에서 발생할 수 있는 다양한 예외를 핸들링하며, 불필요한 정보 노출을 막기 위한 응답 구조도 정비했습니다." + ) + ), + List.of( + new PrAnalysis.CategorizedQuestion( + "보안", + Arrays.asList( + "JWT 토큰을 사용할 때 고려해야 할 보안 취약점은 무엇인가요?", + "리프레시 토큰 저장 위치와 전달 방식에 따른 장단점은 무엇인가요?" + ) + ), + new PrAnalysis.CategorizedQuestion( + "유지보수성", + Arrays.asList( + "토큰 로직을 추상화하거나 모듈화할 때 고려해야 할 요소는 무엇인가요?", + "예외 처리를 공통화할 때 코드의 가독성과 유지보수성을 어떻게 확보할 수 있나요?" + ) + ) + ) + )); +} From a46b31d6c6412414662d6bc5d58e501934c8a594 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 28 Aug 2025 00:48:00 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/devoops/controller/PingController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gss-api-app/src/main/java/com/devoops/controller/PingController.java b/gss-api-app/src/main/java/com/devoops/controller/PingController.java index d32cd59..972334d 100644 --- a/gss-api-app/src/main/java/com/devoops/controller/PingController.java +++ b/gss-api-app/src/main/java/com/devoops/controller/PingController.java @@ -10,6 +10,6 @@ public class PingController { @GetMapping("/api/ping") public ResponseEntity ping() { - return new ResponseEntity<>("pong", HttpStatus.OK); + return new ResponseEntity<>("pong2", HttpStatus.OK); } }