diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java index d193a45..03ee01c 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java @@ -1,22 +1,19 @@ package io.wisoft.prepair.prepair_api.interview.answer.event; import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResult; import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResponse; import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; -import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; import io.wisoft.prepair.prepair_api.interview.answer.repository.AnswerRepository; import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository; -import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; -import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerPersistenceService; import io.wisoft.prepair.prepair_api.interview.answer.service.FeedbackGenerator; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; import io.wisoft.prepair.prepair_api.common.support.SseEmitterManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,10 +22,15 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j @Component @@ -48,55 +50,68 @@ public class AllAnalysisCompletedHandler { @Transactional public void handle(AllAnalysisCompletedEvent event) { UUID answerId = event.answerId(); - deleteTempFile(event.videoPath()); if (event.hasFailed()) { log.error("[종합평가] 분석 실패로 종합평가 생략 - answerId: {}", answerId); - sendFailureToSession(answerId, "분석 중 오류가 발생했습니다."); + failSession(answerId, "분석 중 오류가 발생했습니다."); return; } try { - List feedbacks = feedbackRepository.findByInterviewAnswerId(answerId); - - InterviewFeedback sttFeedback = feedbacks.stream() - .filter(f -> f.getFeedbackType() == FeedbackType.STT) - .findFirst().orElse(null); - - InterviewFeedback videoFeedback = feedbacks.stream() - .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO) - .findFirst().orElse(null); - - if (sttFeedback == null || videoFeedback == null) { - log.error("[종합평가] STT 또는 Video 피드백 없음 - answerId: {}", answerId); + Optional feedbacksOpt = findAnalysisFeedbacks(answerId); + if (feedbacksOpt.isEmpty()) { + failSession(answerId, "분석 결과가 누락되어 종합 평가를 생성할 수 없습니다."); return; } InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null); - if (answer == null) { - return; - } - - String question = answer.getInterviewQuestion().getQuestion(); - - CombinedFeedbackResult result = feedbackGenerator.generateCombined( - question, - sttFeedback.getFeedback(), - videoFeedback.getFeedback() - ); + if (answer == null) return; - answerPersistenceService.saveCombinedFeedback(answerId, result); - log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score()); + saveCombinedFeedback(answerId, answer, feedbacksOpt.get()); + tryGenerateFinalFeedback(answer); - checkAndGenerateFinal(answer); } catch (Exception e) { log.error("[종합평가] 실패 - answerId: {}, error: {}", answerId, e.getMessage(), e); - sendFailureToSession(answerId, "종합 평가 생성 중 오류가 발생했습니다."); + failSession(answerId, "종합 평가 생성 중 오류가 발생했습니다."); } } - private void checkAndGenerateFinal(InterviewAnswer answer) { + private Optional findAnalysisFeedbacks(UUID answerId) { + List feedbacks = feedbackRepository.findByInterviewAnswerId(answerId); + + InterviewFeedback stt = feedbacks.stream() + .filter(f -> f.getFeedbackType() == FeedbackType.STT) + .findFirst() + .orElse(null); + + InterviewFeedback video = feedbacks.stream() + .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO) + .findFirst() + .orElse(null); + + if (stt == null || video == null) { + log.error("[종합평가] STT 또는 Video 피드백 없음 - answerId: {}", answerId); + return Optional.empty(); + } + + return Optional.of(new AnalysisFeedbacks(stt, video)); + } + + private void saveCombinedFeedback(UUID answerId, InterviewAnswer answer, AnalysisFeedbacks feedbacks) { + String question = answer.getInterviewQuestion().getQuestion(); + + CombinedFeedbackResult result = feedbackGenerator.generateCombined( + question, + feedbacks.stt().getFeedback(), + feedbacks.video().getFeedback() + ); + + answerPersistenceService.saveCombinedFeedback(answerId, result); + log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score()); + } + + private void tryGenerateFinalFeedback(InterviewAnswer answer) { InterviewSession session = answer.getInterviewQuestion().getInterviewSession(); if (session == null) { log.warn("[최종평가] 세션 없음 - answerId: {}", answer.getId()); @@ -104,36 +119,64 @@ private void checkAndGenerateFinal(InterviewAnswer answer) { } UUID sessionId = session.getId(); + if (!isFinalFeedbackReady(sessionId, session.getTotalQuestionCount())) return; + + List questions = questionRepository.findByInterviewSessionId(sessionId); + + Map answerMap = answerRepository.findBySessionId(sessionId).stream() + .collect(Collectors.toMap(a -> a.getInterviewQuestion().getId(), a -> a)); + + Map> feedbackMap = feedbackRepository.findAllBySessionId(sessionId).stream() + .collect(Collectors.groupingBy(f -> f.getInterviewAnswer().getId())); + + FinalFeedbackData data = buildFinalData(questions, answerMap, feedbackMap); + FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(data.promptInput()); + + completeSession(session, data, finalResult); + } + + private boolean isFinalFeedbackReady(UUID sessionId, int totalQuestionCount) { long combinedCount = feedbackRepository.countBySessionIdAndFeedbackType(sessionId, FeedbackType.COMBINED); - if (combinedCount < session.getTotalQuestionCount()) { - log.info("[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}", sessionId, combinedCount, session.getTotalQuestionCount()); - return; + if (combinedCount < totalQuestionCount) { + log.info("[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}", sessionId, combinedCount, totalQuestionCount); + return false; } + return true; + } - List questions = questionRepository.findByInterviewSessionId(sessionId); - + private FinalFeedbackData buildFinalData( + List questions, + Map answerMap, + Map> feedbackMap + ) { StringBuilder promptInput = new StringBuilder(); List questionFeedbacks = new ArrayList<>(); int totalScore = 0; for (InterviewQuestion q : questions) { - InterviewAnswer ans = answerRepository.findByInterviewQuestionId(q.getId()).orElse(null); + InterviewAnswer ans = answerMap.get(q.getId()); if (ans == null) continue; - List answerFeedbacks = feedbackRepository.findByInterviewAnswerId(ans.getId()); + List answerFeedbacks = feedbackMap.getOrDefault(ans.getId(), List.of()); InterviewFeedback combined = answerFeedbacks.stream() - .filter(f -> f.getFeedbackType() == FeedbackType.COMBINED).findFirst().orElse(null); + .filter(f -> f.getFeedbackType() == FeedbackType.COMBINED) + .findFirst() + .orElse(null); if (combined == null) continue; String sttFeedbackStr = answerFeedbacks.stream() - .filter(f -> f.getFeedbackType() == FeedbackType.STT).findFirst() - .map(InterviewFeedback::getFeedback).orElse(null); + .filter(f -> f.getFeedbackType() == FeedbackType.STT) + .findFirst() + .map(InterviewFeedback::getFeedback) + .orElse(null); String videoFeedbackStr = answerFeedbacks.stream() - .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO).findFirst() - .map(InterviewFeedback::getFeedback).orElse(null); + .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO) + .findFirst() + .map(InterviewFeedback::getFeedback) + .orElse(null); promptInput.append("질문: ").append(q.getQuestion()).append("\n"); promptInput.append("종합 평가: ").append(combined.getFeedback()).append("\n"); @@ -152,23 +195,38 @@ private void checkAndGenerateFinal(InterviewAnswer answer) { } int finalScore = questionFeedbacks.isEmpty() ? 0 : totalScore / questionFeedbacks.size(); + return new FinalFeedbackData(promptInput.toString(), questionFeedbacks, finalScore); + } - FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(promptInput.toString()); + private void completeSession(InterviewSession session, FinalFeedbackData data, FinalFeedbackResult finalResult) { + UUID sessionId = session.getId(); - session.complete(finalScore, finalResult.finalFeedback()); + session.complete(data.finalScore(), finalResult.finalFeedback()); sessionRepository.save(session); FinalFeedbackResponse response = new FinalFeedbackResponse( sessionId, - finalScore, + data.finalScore(), finalResult.finalFeedback(), - questionFeedbacks + data.questionFeedbacks() ); sseEmitterManager.send(sessionId, "final-complete", response); sseEmitterManager.complete(sessionId); - log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, finalScore); + log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, data.finalScore()); + } + + private void failSession(UUID answerId, String message) { + InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null); + if (answer == null || answer.getInterviewQuestion().getInterviewSession() == null) return; + + InterviewSession session = answer.getInterviewQuestion().getInterviewSession(); + session.fail(); + sessionRepository.save(session); + + sseEmitterManager.send(session.getId(), "analysis-failed", Map.of("message", message)); + sseEmitterManager.complete(session.getId()); } private void deleteTempFile(Path videoPath) { @@ -180,15 +238,16 @@ private void deleteTempFile(Path videoPath) { } } - private void sendFailureToSession(UUID answerId, String message) { - InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null); - if (answer != null && answer.getInterviewQuestion().getInterviewSession() != null) { - InterviewSession session = answer.getInterviewQuestion().getInterviewSession(); - session.fail(); - sessionRepository.save(session); + private record AnalysisFeedbacks( + InterviewFeedback stt, + InterviewFeedback video + ) { + } - sseEmitterManager.send(session.getId(), "analysis-failed", Map.of("message", message)); - sseEmitterManager.complete(session.getId()); - } + private record FinalFeedbackData( + String promptInput, + List questionFeedbacks, + int finalScore + ) { } -} +} \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java index 1c99c6c..9a698e9 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java @@ -1,10 +1,13 @@ package io.wisoft.prepair.prepair_api.interview.answer.repository; import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; + +import java.util.List; import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface AnswerRepository extends JpaRepository { @@ -14,5 +17,8 @@ public interface AnswerRepository extends JpaRepository { "WHERE a.id = :answerId") Optional findByIdWithQuestionAndSession(UUID answerId); - Optional findByInterviewQuestionId(UUID questionId); + @Query("SELECT a FROM InterviewAnswer a " + + "JOIN FETCH a.interviewQuestion q " + + "WHERE q.interviewSession.id = :sessionId") + List findBySessionId(@Param("sessionId") UUID sessionId); } diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java index b760640..b95dc89 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java @@ -2,11 +2,14 @@ import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; + import java.util.List; import java.util.Optional; import java.util.UUID; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface FeedbackRepository extends JpaRepository { List findByInterviewAnswerId(UUID answerId); @@ -16,5 +19,9 @@ public interface FeedbackRepository extends JpaRepository findByInterviewAnswerIdAndFeedbackType(UUID answerId, FeedbackType feedbackType); + @Query("SELECT f FROM InterviewFeedback f " + + "JOIN FETCH f.interviewAnswer a " + + "JOIN FETCH a.interviewQuestion q " + + "WHERE q.interviewSession.id = :sessionId") + List findAllBySessionId(@Param("sessionId") UUID sessionId); }