diff --git a/gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java b/gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java deleted file mode 100644 index dbebb821..00000000 --- a/gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.devoops.event; - -import java.time.LocalDateTime; - -public record AnalyzePrEvent( - Boolean isMerged, - long pullRequestId, - String diffUrl, - String title, - String description, - String label, - long repositoryId, - long userId, - LocalDateTime mergedAt -) { - -} diff --git a/gss-api-app/src/main/java/com/devoops/event/UpdateAnswerEvent.java b/gss-api-app/src/main/java/com/devoops/event/UpdateAnswerEvent.java new file mode 100644 index 00000000..56dd095a --- /dev/null +++ b/gss-api-app/src/main/java/com/devoops/event/UpdateAnswerEvent.java @@ -0,0 +1,18 @@ +package com.devoops.event; + +import com.devoops.domain.entity.github.answer.Answer; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class UpdateAnswerEvent extends ApplicationEvent { + + private final Answer answer; + private final long userId; + + public UpdateAnswerEvent(Object source, Answer answer, long userId) { + super(source); + this.answer = answer; + this.userId = userId; + } +} diff --git a/gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java b/gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java new file mode 100644 index 00000000..ce509317 --- /dev/null +++ b/gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java @@ -0,0 +1,25 @@ +package com.devoops.event.listener; + +import com.devoops.domain.entity.github.answer.Answer; +import com.devoops.event.UpdateAnswerEvent; +import com.devoops.service.answerranking.AnswerRankingService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class AnswerEventListener { + + private final AnswerRankingService answerRankingService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void updateAnswer(UpdateAnswerEvent updateAnswerEvent) { + Answer answer = updateAnswerEvent.getAnswer(); + long userId = updateAnswerEvent.getUserId(); + answerRankingService.push(answer, userId); + } +} diff --git a/gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java b/gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java index 336e964b..3146d20b 100644 --- a/gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java +++ b/gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java @@ -2,11 +2,6 @@ import com.devoops.dto.AppWebhookEventRequest; -import com.devoops.exception.custom.GssException; -import com.devoops.exception.errorcode.ErrorCode; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.annotation.PostConstruct; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -19,15 +14,9 @@ public class PrAnalysisPublisher { private final RedisTemplate redisTemplate; private final ChannelTopic channelTopic; - private final ObjectMapper objectMapper; public void publish(List eventList) { -// try { -// String message = objectMapper.writeValueAsString(eventList); - redisTemplate.convertAndSend(channelTopic.getTopic(), eventList); -// } catch (JsonProcessingException e) { -// throw new GssException(ErrorCode.REDIS_PUBLISH_ERROR); -// } + redisTemplate.convertAndSend(channelTopic.getTopic(), eventList); } } diff --git a/gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java b/gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java index 74473e83..85b7c309 100644 --- a/gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java +++ b/gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java @@ -7,13 +7,16 @@ import com.devoops.domain.entity.github.pr.RecordStatus; import com.devoops.domain.entity.user.User; import com.devoops.dto.request.AnswerPutRequests; +import com.devoops.event.UpdateAnswerEvent; import com.devoops.service.answer.AnswerService; import com.devoops.service.answerranking.AnswerRankingService; import com.devoops.service.pullrequest.PullRequestService; import com.devoops.service.question.QuestionService; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -22,7 +25,7 @@ public class QuestionFacadeService { private final PullRequestService pullRequestService; private final QuestionService questionService; private final AnswerService answerService; - private final AnswerRankingService answerRankingService; + private final ApplicationEventPublisher eventPublisher; public Answer initializeAnswer(long questionId, User user) { PullRequest pullRequest = pullRequestService.findByQuestionId(questionId); @@ -32,12 +35,14 @@ public Answer initializeAnswer(long questionId, User user) { return questionService.initializeAnswer(questionId, user); } + @Transactional public Answer updateAnswer(long answerId, String updateContent, long userId) { Answer answer = questionService.updateAnswer(answerId, updateContent); - answerRankingService.push(answer, userId); + eventPublisher.publishEvent(new UpdateAnswerEvent(this, answer, userId)); return answer; } + @Transactional public Answers updateAllAnswers(AnswerPutRequests updateRequests) { List updateCommands = updateRequests.answers() .stream() diff --git a/gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java b/gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java index 4ce86bcc..0f943c54 100644 --- a/gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java +++ b/gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java @@ -18,6 +18,8 @@ import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.springframework.beans.factory.annotation.Autowired; class AnswerRankingServiceTest extends BaseServiceTest { @@ -47,6 +49,21 @@ class Push { assertThat(userRanking.getRankings()).hasSize(1); } + @Test + void 답변내용이_비어있으면_랭킹에_반영하지_않는다() { + User user = userGenerator.generate("김건우"); + GithubRepository repo = repoGenerator.generate(user, "건우의 레포"); + PullRequest pullRequest = pullRequestGenerator.generate("최초 PR", RecordStatus.PENDING, ProcessingStatus.DONE, + repo, LocalDateTime.now()); + Question question1 = questionGenerator.generate(pullRequest, "질문1"); + Answer answer = answerGenerator.generate(question1, ""); + + answerRankingService.push(answer, user.getId()); + + AnswerRankings userRanking = answerRankingDomainRepository.findAllByUserId(user.getId()); + assertThat(userRanking.getRankings()).isEmpty(); + } + @Test void 유저의_PR_랭킹을_모두_가져온다() { User user = userGenerator.generate("김건우"); diff --git a/gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java b/gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java index 4f41c1ff..9baa776c 100644 --- a/gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java +++ b/gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java @@ -14,11 +14,15 @@ import com.devoops.domain.repository.github.pr.PullRequestDomainRepository; import com.devoops.dto.request.AnswerPutRequest; import com.devoops.dto.request.AnswerPutRequests; +import com.devoops.event.UpdateAnswerEvent; import java.time.LocalDateTime; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; class QuestionFacadeServiceTest extends BaseServiceTest { @@ -113,4 +117,33 @@ class UpdateAllAnswers { ); } } + + @Nested + @RecordApplicationEvents + class UpdateAnswers { + + @Autowired + private ApplicationEvents applicationEvents; + + @BeforeEach + void setUp() { + applicationEvents.clear(); + } + + @Test + void 회고를_업데이트_시_랭킹_갱신_이벤트를_발행한다() { + User user = userGenerator.generate("김건우"); + GithubRepository repo = repoGenerator.generate(user, "건우의 레포"); + PullRequest pr1 = pullRequestGenerator.generate("PR1", RecordStatus.PENDING, ProcessingStatus.DONE, repo, LocalDateTime.now()); + Question question1 = questionGenerator.generate(pr1, "질문1"); + Answer answer1 = answerGenerator.generate(question1, "answer1"); + + questionFacadeService.updateAnswer(answer1.getId(), "updateContent", user.getId()); + + assertThat(applicationEvents.stream(UpdateAnswerEvent.class)) + .hasSize(1) + .allMatch(event -> event.getAnswer().getId().equals(answer1.getId())) + .allMatch(event -> event.getUserId() == user.getId()); + } + } } 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 5fac40da..6bccf5c8 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 @@ -8,6 +8,7 @@ dev-oops: - "summaryDetail"은 변경 내용을 항목별로 요약한 제목(title) + 설명(description) 쌍으로 구성해. - "category"는 기술적인 관점에서 PR 코드 변경 내용을 반영하여 선택해 (예: 성능, 보안, 확장성, 유지보수성, 테스트 등) - "question"은 각 category에 대해 기술 면접에서 사용할 수 있는 질문이어야 해. + - "question"은 가능한 255자가 넘지 않도록 짧고 명료하게 질문해. - 각 질문들은 반드시 PR 코드 변경 내용("diff")을 인용해서 생성해. - "diff"를 굉장히 자세하게 분석하고 몇몇 질문에는 코드를 반영해서 만들어줘 - 질문 수는 카테고리마다 3개 이상 만들어. diff --git a/gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java b/gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java index e4695cae..8c755439 100644 --- a/gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java +++ b/gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java @@ -1,5 +1,6 @@ package com.devoops.domain.entity.github.answer; +import io.micrometer.common.util.StringUtils; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -16,4 +17,8 @@ public class Answer { public static Answer initialize(long questionId) { return new Answer(null, questionId, INITIALIZED_ANSWER_CONTENT); } + + public boolean isBlank() { + return StringUtils.isBlank(content); + } } diff --git a/gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java b/gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java index d25296dd..dbb41c41 100644 --- a/gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java +++ b/gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java @@ -14,6 +14,4 @@ public interface AnswerRankingDomainRepository { AnswerRanking update(long pullRequestId, long questionId); void deleteById(long id); - - void deleteAllInPullRequests(PullRequests pullRequests); } diff --git a/gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java b/gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java index c16b88b1..3c94c01f 100644 --- a/gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java +++ b/gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java @@ -26,7 +26,7 @@ public class AnswerRankingEntity { private long questionId; @NotBlank - @Column(name = "content") + @Column(name = "content", columnDefinition = "TEXT") private String questionContent; @Column(name = "pull_request_id") diff --git a/gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java b/gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java index 55679d49..5e0e00f9 100644 --- a/gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java +++ b/gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java @@ -74,16 +74,6 @@ public void deleteById(long id) { answerRankingJpaRepository.deleteById(id); } - @Override - @Transactional - public void deleteAllInPullRequests(PullRequests pullRequests) { - List pullRequestIds = pullRequests.getValues() - .stream() - .map(PullRequest::getId) - .toList(); - answerRankingJpaRepository.deleteByPullRequestIdIn(pullRequestIds); - } - private QuestionEntity findQuestionById(long questionId) { return questionJpaRepository.findById(questionId) .orElseThrow(() -> new GssException(ErrorCode.QUESTION_NOT_FOUND)); diff --git a/gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java b/gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java index 1b1b2b73..06b6bae6 100644 --- a/gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java +++ b/gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java @@ -22,6 +22,9 @@ public AnswerRankings findUserRanking(long userId) { } public void push(Answer answer, long userId) { + if(answer.isBlank()) { + return; + } AnswerRankings answerRankings = findUserRanking(userId); Question question = questionDomainRepository.findById(answer.getQuestionId());