From 45a474209324529889906e83810a258489a907ae Mon Sep 17 00:00:00 2001 From: coli Date: Fri, 29 Aug 2025 22:31:15 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20aiChargeService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/pranalysis/AiChargeService.java | 27 +++++++++++++++++++ .../devoops/event/QuestionEventListener.java | 6 ++--- ...vice.java => PrAnalysisFacadeService.java} | 12 +++------ 3 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java rename gss-mcp-app/src/main/java/com/devoops/service/pranalysis/{PrAnalysisService.java => PrAnalysisFacadeService.java} (74%) diff --git a/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java b/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java new file mode 100644 index 0000000..00f2fe8 --- /dev/null +++ b/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java @@ -0,0 +1,27 @@ +package com.devoops.service.pranalysis; + +import com.devoops.domain.entity.analysis.AiCharge; +import com.devoops.domain.repository.analysis.AiChargeRepository; +import java.time.LocalDate; +import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AiChargeService { + + private final AiChargeRepository chargeRepository; + + public AiCharge getMonthlyCharge() { + ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); + LocalDate today = LocalDate.now(seoulZoneId); + return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); + } + + public void addCharge(double consumedCharge) { + ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); + LocalDate today = LocalDate.now(seoulZoneId); + chargeRepository.addCharge(today.getYear(), today.getMonthValue(), consumedCharge); + } +} diff --git a/gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java b/gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java index d87b1b4..35e65b2 100644 --- a/gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java +++ b/gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java @@ -7,7 +7,7 @@ import com.devoops.dto.AppWebhookEventRequest; import com.devoops.dto.request.AdaptedAnalyzePrResponse; import com.devoops.dto.response.PrAnalysis; -import com.devoops.service.pranalysis.PrAnalysisService; +import com.devoops.service.pranalysis.PrAnalysisFacadeService; import com.devoops.service.pullrequest.PullRequestService; import com.devoops.service.question.QuestionService; import java.util.List; @@ -22,7 +22,7 @@ public class QuestionEventListener { private final PullRequestService pullRequestService; private final QuestionService questionService; - private final PrAnalysisService prAnalysisService; + private final PrAnalysisFacadeService prAnalysisFacadeService; @Async @EventListener(QuestionCreateEvent.class) @@ -31,7 +31,7 @@ public void createQuestion(QuestionCreateEvent questionCreateEvent) { GithubToken githubToken = questionCreateEvent.getToken(); PullRequest readyPullRequest = questionCreateEvent.getInitializedPullRequest(); - AdaptedAnalyzePrResponse adaptedAnalyzePrResponse = prAnalysisService.analyzePullRequest(request, githubToken); + AdaptedAnalyzePrResponse adaptedAnalyzePrResponse = prAnalysisFacadeService.analyzePullRequest(request, githubToken); PullRequest updatedPullRequest = pullRequestService.updateAnalyzeResult( readyPullRequest.getId(), 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/PrAnalysisFacadeService.java similarity index 74% rename from gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisService.java rename to gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisFacadeService.java index c1673e6..2747059 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/PrAnalysisFacadeService.java @@ -8,24 +8,20 @@ import com.devoops.domain.repository.analysis.AiChargeRepository; import com.devoops.dto.AppWebhookEventRequest; import com.devoops.dto.request.AdaptedAnalyzePrResponse; -import java.time.LocalDate; -import java.time.ZoneId; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class PrAnalysisService { +public class PrAnalysisFacadeService { private final GithubAdaptor githubAdaptor; private final PrAnalysisAdapter prAnalysisAdapter; - private final AiChargeRepository chargeRepository; + private final AiChargeService aiChargeService; public AdaptedAnalyzePrResponse analyzePullRequest(AppWebhookEventRequest request, GithubToken githubToken) { String diff = githubAdaptor.getCodeChangeHistory(request.diffUrl(), githubToken.getToken()); - ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); - LocalDate today = LocalDate.now(seoulZoneId); - AiCharge aiCharge = chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); + AiCharge aiCharge = aiChargeService.getMonthlyCharge(); OpenAiModel aiModel = OpenAiModel.getModelByUsage(aiCharge.getCharge()); AdaptedAnalyzePrResponse result = prAnalysisAdapter.analyze( @@ -36,7 +32,7 @@ public AdaptedAnalyzePrResponse analyzePullRequest(AppWebhookEventRequest reques ); double consumedCharge = aiModel.getCharge(result.promptTokens(), result.completionTokens()); - chargeRepository.addCharge(today.getYear(), today.getMonthValue(), consumedCharge); + aiChargeService.addCharge(consumedCharge); return result; } } From 3208ad31a23d4ada5fe76dc3b923f4836ccb99af Mon Sep 17 00:00:00 2001 From: coli Date: Fri, 29 Aug 2025 22:34:51 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EB=8B=A4=EC=9D=8C=EB=8B=AC=20ai=20?= =?UTF-8?q?=EB=B9=84=EC=9A=A9=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devoops/exception/errorcode/ErrorCode.java | 3 ++- .../repository/analysis/AiChargeRepository.java | 2 ++ .../analysis/AiChargeRepositoryImpl.java | 15 +++++++++++---- .../service/pranalysis/AiChargeService.java | 17 +++++++++++++---- 4 files changed, 28 insertions(+), 9 deletions(-) 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 ce93fb5..03244ab 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,7 +40,8 @@ public enum ErrorCode { REDIS_SUBSCRIBE_ERROR(500, "레디스 이벤트 수신 과정에서 문제가 생겼습니다"), GITHUB_CLIENT_ERROR(500, "깃허브 클라이언트 소통과정에 문제가 발생했습니다"), AI_RESPONSE_PARSING_ERROR(500, "AI로부터 온 질문 생성을 파싱하는 과정에 오류가 발생했습니다"), - AI_CREATE_QUESTION_ERROR(500, "AI 질문 생성과정에 오류가 발생했습니다") + AI_CREATE_QUESTION_ERROR(500, "AI 질문 생성과정에 오류가 발생했습니다"), + AI_CHARGE_NOT_FOUND(500, "당월 AI 비용을 찾을 없습니다.") ; private final int statusCode; diff --git a/gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java b/gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java index e4133e8..4adc6d0 100644 --- a/gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java +++ b/gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java @@ -7,4 +7,6 @@ public interface AiChargeRepository { AiCharge getByYearAndMonth(int year, int month); void addCharge(int year, int month, double charge); + + AiCharge save(AiCharge charge); } diff --git a/gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java b/gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java index 1403f44..636e500 100644 --- a/gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java +++ b/gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java @@ -2,6 +2,8 @@ import com.devoops.domain.entity.analysis.AiCharge; import com.devoops.domain.repository.analysis.AiChargeRepository; +import com.devoops.exception.custom.GssException; +import com.devoops.exception.errorcode.ErrorCode; import com.devoops.jpa.entity.analysis.AiChargeEntity; import java.math.BigDecimal; import lombok.RequiredArgsConstructor; @@ -17,10 +19,8 @@ public class AiChargeRepositoryImpl implements AiChargeRepository { @Override public AiCharge getByYearAndMonth(int year, int month) { return chargeJpaRepository.findByYearAndMonth(year, month) - .orElseGet(() -> { - AiCharge initializeCharge = new AiCharge(year, month, BigDecimal.ZERO); - return chargeJpaRepository.save(AiChargeEntity.from(initializeCharge)); - }).toDomainEntity(); + .orElseThrow(() -> new GssException(ErrorCode.AI_CHARGE_NOT_FOUND)) + .toDomainEntity(); } @Override @@ -28,4 +28,11 @@ public AiCharge getByYearAndMonth(int year, int month) { public void addCharge(int year, int month, double charge) { chargeJpaRepository.updateChargeById(year, month, charge); } + + @Override + @Transactional + public AiCharge save(AiCharge charge) { + return chargeJpaRepository.save(AiChargeEntity.from(charge)) + .toDomainEntity(); + } } diff --git a/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java b/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java index 00f2fe8..a6dfdc5 100644 --- a/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java +++ b/gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java @@ -2,7 +2,9 @@ import com.devoops.domain.entity.analysis.AiCharge; import com.devoops.domain.repository.analysis.AiChargeRepository; +import java.math.BigDecimal; import java.time.LocalDate; +import java.time.YearMonth; import java.time.ZoneId; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,17 +13,24 @@ @RequiredArgsConstructor public class AiChargeService { + private static final ZoneId SEOUL_ZONE_ID = ZoneId.of("Asia/Seoul"); + private final AiChargeRepository chargeRepository; + public AiCharge initializeNextMonth() { + LocalDate now = LocalDate.now(SEOUL_ZONE_ID); + YearMonth nextMonth = YearMonth.from(now).plusMonths(1); + AiCharge aiCharge = new AiCharge(nextMonth.getYear(), nextMonth.getMonthValue(), BigDecimal.ZERO); + return chargeRepository.save(aiCharge); + } + public AiCharge getMonthlyCharge() { - ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); - LocalDate today = LocalDate.now(seoulZoneId); + LocalDate today = LocalDate.now(SEOUL_ZONE_ID); return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); } public void addCharge(double consumedCharge) { - ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); - LocalDate today = LocalDate.now(seoulZoneId); + LocalDate today = LocalDate.now(SEOUL_ZONE_ID); chargeRepository.addCharge(today.getYear(), today.getMonthValue(), consumedCharge); } } From 28d0854717c50bbf17e5d758c3620fac4824fcde Mon Sep 17 00:00:00 2001 From: coli Date: Fri, 29 Aug 2025 22:39:03 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EB=8B=A4=EC=9D=8C=EB=8B=AC=20ai=20?= =?UTF-8?q?=EB=B9=84=EC=9A=A9=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A5=B4=EB=A7=81=20=EB=A1=9C=EC=A7=81=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/devoops/config/SchedulingConfig.java | 10 ++++++++++ .../service/pranalysis/AiChargeScheduler.java | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 gss-mcp-app/src/main/java/com/devoops/config/SchedulingConfig.java create mode 100644 gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java diff --git a/gss-mcp-app/src/main/java/com/devoops/config/SchedulingConfig.java b/gss-mcp-app/src/main/java/com/devoops/config/SchedulingConfig.java new file mode 100644 index 0000000..ac96656 --- /dev/null +++ b/gss-mcp-app/src/main/java/com/devoops/config/SchedulingConfig.java @@ -0,0 +1,10 @@ +package com.devoops.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulingConfig { + +} diff --git a/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java b/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java new file mode 100644 index 0000000..7d62355 --- /dev/null +++ b/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java @@ -0,0 +1,17 @@ +package com.devoops.service.pranalysis; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +class AiChargeScheduler { + + private final AiChargeService aiChargeService; + + @Scheduled(cron = "0 55 23 L * ?") + public void createNextMonthCharge() { + aiChargeService.initializeNextMonth(); + } +} From 987bbae9f3fb3690cbbaa2493f6920288b0224f9 Mon Sep 17 00:00:00 2001 From: coli Date: Fri, 29 Aug 2025 22:48:19 +0900 Subject: [PATCH 4/6] =?UTF-8?q?test:=20=EB=8B=A4=EC=9D=8C=EB=8B=AC=20ai=20?= =?UTF-8?q?=EB=B9=84=EC=9A=A9=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A5=B4=EB=A7=81=20=EB=A1=9C=EC=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pranalysis/AiChargeSchedulerTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java diff --git a/gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java b/gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java new file mode 100644 index 0000000..bacea55 --- /dev/null +++ b/gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java @@ -0,0 +1,47 @@ +package com.devoops.service.pranalysis; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.devoops.BaseMcpTest; +import com.devoops.domain.entity.analysis.AiCharge; +import com.devoops.domain.repository.analysis.AiChargeRepository; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class AiChargeSchedulerTest extends BaseMcpTest { + + @Autowired + private AiChargeScheduler aiChargeScheduler; + + @Autowired + private AiChargeRepository aiChargeRepository; + + @Nested + class CreateNextMonthCharge { + + @Test + void 다음달_비용을_초기화한다() { + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + YearMonth yearMonth = YearMonth.from(LocalDate.now(zoneId)); + YearMonth nextMonth = yearMonth.plusMonths(1); + + aiChargeScheduler.createNextMonthCharge(); + + AiCharge nextMonthAiCharge = aiChargeRepository.getByYearAndMonth( + nextMonth.getYear(), + nextMonth.getMonthValue() + ); + assertAll( + () -> assertThat(nextMonthAiCharge.getYear()).isEqualTo(nextMonth.getYear()), + () -> assertThat(nextMonthAiCharge.getMonth()).isEqualTo(nextMonth.getMonthValue()), + () -> assertThat(nextMonthAiCharge.getCharge().doubleValue()).isEqualTo(0.0) + ); + } + } + +} From f48f54b15df6f229b87544bfb67f948717543fad Mon Sep 17 00:00:00 2001 From: coli Date: Fri, 29 Aug 2025 22:55:31 +0900 Subject: [PATCH 5/6] =?UTF-8?q?test:=20=EC=8B=A4=ED=8C=A8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/analysis/AiChargeRepositoryTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java b/gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java index a5c41dd..def11eb 100644 --- a/gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java +++ b/gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java @@ -1,10 +1,13 @@ package com.devoops.repository.analysis; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.devoops.BaseServiceTest; import com.devoops.domain.entity.analysis.AiCharge; import com.devoops.domain.repository.analysis.AiChargeRepository; +import com.devoops.exception.custom.GssException; +import com.devoops.exception.errorcode.ErrorCode; import java.time.LocalDate; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,13 +33,14 @@ class GetByMonth { } @Test - void 가져올_요금이_없다면_초기화한다() { + void 가져올_요금이_없다면_에러가_발생한다() { LocalDate localDate = LocalDate.now(); - AiCharge actual = chargeRepository.getByYearAndMonth(localDate.getYear(), localDate.getMonthValue()); - - assertThat(actual.getCharge().doubleValue()).isEqualTo(0.0); + assertThatThrownBy(() -> chargeRepository.getByYearAndMonth(localDate.getYear(), localDate.getMonthValue())) + .isInstanceOf(GssException.class) + .hasMessage(ErrorCode.AI_CHARGE_NOT_FOUND.getMessage()); } + } @Nested From 9c82401a207b90f67752bad242ddeb3775b62af6 Mon Sep 17 00:00:00 2001 From: coli Date: Fri, 29 Aug 2025 23:38:09 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=9F=AC=20zoneId=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/devoops/service/pranalysis/AiChargeScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java b/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java index 7d62355..f65081a 100644 --- a/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java +++ b/gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java @@ -10,7 +10,7 @@ class AiChargeScheduler { private final AiChargeService aiChargeService; - @Scheduled(cron = "0 55 23 L * ?") + @Scheduled(cron = "0 55 23 L * ?", zone = "Asia/Seoul") public void createNextMonthCharge() { aiChargeService.initializeNextMonth(); }