From 3e8f4c0e69f92d9e8adfafe7a73556799e9169b2 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Thu, 4 Jun 2026 23:53:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=B6=94=EC=B2=9C=20=EC=A4=91=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EA=B0=80=20=EC=98=A4=EB=A5=98=EB=82=98?= =?UTF-8?q?=EB=8F=84=20=EC=A0=84=EC=B2=B4=20=EB=A7=A4=EC=B9=AD=EC=9D=84=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=EC=8B=9C=ED=82=A4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DecisionCommitMatchService.java | 27 ++++++++++++++++--- .../service/MeetingAnalysisService.java | 6 +++++ .../exception/GeneralException.java | 9 ++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/whylog/server/domain/decision/service/DecisionCommitMatchService.java b/src/main/java/com/whylog/server/domain/decision/service/DecisionCommitMatchService.java index bd11f48..fd9b58a 100644 --- a/src/main/java/com/whylog/server/domain/decision/service/DecisionCommitMatchService.java +++ b/src/main/java/com/whylog/server/domain/decision/service/DecisionCommitMatchService.java @@ -38,9 +38,11 @@ import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class DecisionCommitMatchService { @@ -227,13 +229,21 @@ private void saveRecommendations(Decision decision, List repositoryIds, Js Map applicationsById = findAndValidateApplications(decision, candidatesByKey.values()); Map commitsById = findAndValidateCommits(repositoryIds, candidatesByKey.values()); + List validCandidates = candidatesByKey.values().stream() + .filter(candidate -> candidate.resolvedCommitId() != null) + .toList(); // 새 추천 스냅샷을 저장하기 전에 기존 추천 결과를 통째로 교체 applicationCommitsRepository.deleteByDecisionId(decision.getId()); decisionCommitsRepository.deleteByDecisionId(decision.getId()); + if (validCandidates.isEmpty()) { + decisionRepository.updateReliabilityScore(decision.getId(), null); + return; + } + Map decisionCommitsByCommitId = new LinkedHashMap<>(); - for (DecisionCommitMatchCandidate candidate : candidatesByKey.values()) { + for (DecisionCommitMatchCandidate candidate : validCandidates) { Application application = applicationsById.get(candidate.resolvedApplicationId()); Commit commit = commitsById.get(candidate.resolvedCommitId()); @@ -298,9 +308,18 @@ private Map findAndValidateCommits(List repositoryIds, Map commitsById = new LinkedHashMap<>(); for (DecisionCommitMatchCandidate candidate : candidates) { - Commit commit = resolveCommit(candidate, repositoryIdSet); - candidate.resolveCommitId(commit.getId()); - commitsById.put(commit.getId(), commit); + try { + Commit commit = resolveCommit(candidate, repositoryIdSet); + candidate.resolveCommitId(commit.getId()); + commitsById.put(commit.getId(), commit); + } catch (ErrorHandler exception) { + log.warn("추천 커밋 후보를 찾을 수 없어 제외한다: repositoryId={}, commitHash={}, commitId={}, errorCode={}, message={}", + candidate.repositoryId(), + candidate.commitHash(), + candidate.commitId(), + exception.getErrorReason().getCode(), + exception.getErrorReason().getMessage()); + } } return commitsById; diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java index 2b237a1..cca9c13 100644 --- a/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java @@ -34,6 +34,8 @@ import com.whylog.server.global.external.fast.dto.response.TranscribeApplicationRunResponse; import com.whylog.server.global.external.fast.exception.FastApiErrorCode; import com.whylog.server.global.external.fast.exception.FastApiException; +import com.whylog.server.global.apiPayload.code.ErrorReasonDTO; +import com.whylog.server.global.apiPayload.exception.GeneralException; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; @@ -387,6 +389,10 @@ private void matchApplicationCommitsSafely(Meeting meeting, SavedApplications sa decisionCommitMatchService.matchApplicationCommits(savedApplications.decisionId()); log.info("적용사항-커밋 추천 매칭 완료: meetingId={}, decisionId={}", meeting.getId(), savedApplications.decisionId()); + } catch (GeneralException exception) { + ErrorReasonDTO reason = exception.getErrorReason(); + log.error("적용사항-커밋 추천 매칭 실패: meetingId={}, decisionId={}, errorCode={}, message={}", + meeting.getId(), savedApplications.decisionId(), reason.getCode(), reason.getMessage(), exception); } catch (Exception exception) { log.error("적용사항-커밋 추천 매칭 실패: meetingId={}, decisionId={}", meeting.getId(), savedApplications.decisionId(), exception); diff --git a/src/main/java/com/whylog/server/global/apiPayload/exception/GeneralException.java b/src/main/java/com/whylog/server/global/apiPayload/exception/GeneralException.java index 1967f66..ae53e4a 100644 --- a/src/main/java/com/whylog/server/global/apiPayload/exception/GeneralException.java +++ b/src/main/java/com/whylog/server/global/apiPayload/exception/GeneralException.java @@ -2,15 +2,18 @@ import com.whylog.server.global.apiPayload.code.BaseErrorCode; import com.whylog.server.global.apiPayload.code.ErrorReasonDTO; -import lombok.AllArgsConstructor; import lombok.Getter; @Getter -@AllArgsConstructor public class GeneralException extends RuntimeException { private BaseErrorCode code; + public GeneralException(BaseErrorCode code) { + super(code.getReason().getMessage()); + this.code = code; + } + public ErrorReasonDTO getErrorReason() { return this.code.getReason(); } @@ -18,4 +21,4 @@ public ErrorReasonDTO getErrorReason() { public ErrorReasonDTO getErrorReasonHttpStatus(){ return this.code.getReasonHttpStatus(); } -} \ No newline at end of file +} From 2d904e38015c33031411b355a80c6424239eac46 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Thu, 4 Jun 2026 23:56:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=B0=8B=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=9E=AC=EC=9A=94=EC=B2=AD=EC=9D=84=20Embedding=20?= =?UTF-8?q?Ready=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/git/entity/CommitAnalysis.java | 7 +- .../git/repository/CommitRepository.java | 4 +- .../git/service/GitCommandServiceImpl.java | 81 ++++++++++++++++--- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java b/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java index ff83465..27c3ef8 100644 --- a/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java +++ b/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java @@ -32,13 +32,18 @@ public class CommitAnalysis extends BaseEntity { @Column(name = "summary", columnDefinition = "TEXT") private String summary; + @Column(name = "embedding_ready", nullable = false) + private boolean embeddingReady; + public static CommitAnalysis create(Commit commit) { CommitAnalysis commitAnalysis = new CommitAnalysis(); commitAnalysis.commit = commit; + commitAnalysis.embeddingReady = false; return commitAnalysis; } - public void updateSummary(String summary) { + public void updateSummary(String summary, boolean embeddingReady) { this.summary = summary; + this.embeddingReady = embeddingReady; } } diff --git a/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java b/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java index 3519e88..1860f43 100644 --- a/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java +++ b/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java @@ -42,9 +42,9 @@ Set findExistingHashes( FROM Commit c LEFT JOIN c.commitAnalysis ca WHERE c.repository.id = :repositoryId - AND ca.id IS NULL + AND (ca.id IS NULL OR ca.embeddingReady = false) """) - List findUnanalyzedCommitsByRepositoryId(@Param("repositoryId") Long repositoryId); + List findEmbeddingNotReadyCommitsByRepositoryId(@Param("repositoryId") Long repositoryId); @Query(""" SELECT c.id diff --git a/src/main/java/com/whylog/server/domain/git/service/GitCommandServiceImpl.java b/src/main/java/com/whylog/server/domain/git/service/GitCommandServiceImpl.java index ca8909e..f0b1612 100644 --- a/src/main/java/com/whylog/server/domain/git/service/GitCommandServiceImpl.java +++ b/src/main/java/com/whylog/server/domain/git/service/GitCommandServiceImpl.java @@ -272,8 +272,8 @@ private void syncCommits(GHRepository ghRepository, Repository repository, Local log.info("새로 동기화할 커밋이 없습니다: {}", ghRepository.getFullName()); } - List unanalyzedCommits = commitRepository.findUnanalyzedCommitsByRepositoryId(repository.getId()); - List analyzeTargets = mergeAnalyzeTargets(savedCommits, unanalyzedCommits); + List embeddingNotReadyCommits = commitRepository.findEmbeddingNotReadyCommitsByRepositoryId(repository.getId()); + List analyzeTargets = mergeAnalyzeTargets(savedCommits, embeddingNotReadyCommits); if (!analyzeTargets.isEmpty()) { triggerCommitAnalyzeRunsAfterCommit(ghRepository, repository, analyzeTargets); @@ -281,10 +281,10 @@ private void syncCommits(GHRepository ghRepository, Repository repository, Local } } - private List mergeAnalyzeTargets(List savedCommits, List unanalyzedCommits) { + private List mergeAnalyzeTargets(List savedCommits, List embeddingNotReadyCommits) { Map analyzeTargets = new LinkedHashMap<>(); savedCommits.forEach(commit -> analyzeTargets.put(commit.getId(), commit)); - unanalyzedCommits.forEach(commit -> analyzeTargets.putIfAbsent(commit.getId(), commit)); + embeddingNotReadyCommits.forEach(commit -> analyzeTargets.putIfAbsent(commit.getId(), commit)); return new ArrayList<>(analyzeTargets.values()); } @@ -371,7 +371,7 @@ private void createCommitAnalyzeRun(GHRepository ghRepository, Repository reposi } /** - * FastAPI 분석 run 상태를 폴링해 summary가 준비된 최종 결과를 가져옵니다. + * FastAPI 분석 run 상태를 폴링해 summary와 embedding이 준비된 최종 결과를 가져옵니다. */ private JsonNode pollCommitAnalyzeRun(String runId) { for (int attempt = 1; attempt <= 120; attempt++) { @@ -387,6 +387,7 @@ private JsonNode pollCommitAnalyzeRun(String runId) { String phase = readText(runResult, "phase"); String error = readText(runResult, "error"); String summary = readNestedText(runResult, "result", "summary"); + boolean embeddingReady = isCommitEmbeddingReady(runResult); if (isFailed(status, phase)) { throw new FastApiException( @@ -395,7 +396,7 @@ private JsonNode pollCommitAnalyzeRun(String runId) { ); } - if (summary != null && !summary.isBlank()) { + if (summary != null && !summary.isBlank() && embeddingReady) { return runResult; } @@ -409,21 +410,22 @@ private JsonNode pollCommitAnalyzeRun(String runId) { } /** - * 완료된 분석 결과에서 summary를 upsert 저장합니다. + * 완료된 분석 결과에서 summary와 embedding 준비 상태를 upsert 저장합니다. */ private void saveCommitAnalysis(Commit commit, JsonNode runResult) { String summary = readNestedText(runResult, "result", "summary"); + boolean embeddingReady = isCommitEmbeddingReady(runResult); - if (summary == null || summary.isBlank()) { + if (summary == null || summary.isBlank() || !embeddingReady) { throw new FastApiException(FastApiErrorCode.FAST_API_RESPONSE_EMPTY); } CommitAnalysis commitAnalysis = commitAnalysisRepository.findByCommitId(commit.getId()) .orElseGet(() -> CommitAnalysis.create(commit)); - commitAnalysis.updateSummary(summary); + commitAnalysis.updateSummary(summary, embeddingReady); commitAnalysisRepository.save(commitAnalysis); - log.info("커밋 분석 저장 완료: commitHash={}", commit.getHash()); + log.info("커밋 분석 저장 완료: commitHash={}, embeddingReady={}", commit.getHash(), embeddingReady); } /** @@ -433,6 +435,34 @@ private boolean isFailed(String status, String phase) { return "failed".equalsIgnoreCase(status) || "failed".equalsIgnoreCase(phase); } + /** + * FastAPI 커밋 분석 결과의 embedding 준비 여부를 확인합니다. + */ + private boolean isCommitEmbeddingReady(JsonNode runResult) { + String phase = readText(runResult, "phase"); + if ("embedding_ready".equalsIgnoreCase(phase)) { + return true; + } + + Boolean embeddingReady = readBoolean(runResult, "embedding_ready"); + if (embeddingReady != null) { + return embeddingReady; + } + + Boolean camelEmbeddingReady = readBoolean(runResult, "embeddingReady"); + if (camelEmbeddingReady != null) { + return camelEmbeddingReady; + } + + Boolean nestedEmbeddingReady = readNestedBoolean(runResult, "result", "embedding_ready"); + if (nestedEmbeddingReady != null) { + return nestedEmbeddingReady; + } + + Boolean nestedCamelEmbeddingReady = readNestedBoolean(runResult, "result", "embeddingReady"); + return Boolean.TRUE.equals(nestedCamelEmbeddingReady); + } + /** * 커밋 분석 실패가 재시도 가능한 일시적 오류인지 확인합니다. */ @@ -505,6 +535,37 @@ private String readNestedText(JsonNode node, String parentFieldName, String chil return child != null ? child.asText(null) : null; } + /** + * JSON 필드의 Boolean 값을 안전하게 읽습니다. + */ + private Boolean readBoolean(JsonNode node, String fieldName) { + JsonNode value = node != null ? node.get(fieldName) : null; + if (value == null || value.isNull()) { + return null; + } + if (value.isBoolean()) { + return value.asBoolean(); + } + if (value.isTextual()) { + String text = value.asText(); + if ("true".equalsIgnoreCase(text)) { + return true; + } + if ("false".equalsIgnoreCase(text)) { + return false; + } + } + return null; + } + + /** + * 중첩 JSON 필드의 Boolean 값을 안전하게 읽습니다. + */ + private Boolean readNestedBoolean(JsonNode node, String parentFieldName, String childFieldName) { + JsonNode parent = node != null ? node.get(parentFieldName) : null; + return readBoolean(parent, childFieldName); + } + /** * FastAPI 응답의 result가 비어 있으면 예외를 던집니다. */