Skip to content

Commit 048fa2e

Browse files
committed
refactor: AllAnalysisCompletedHandler 책임 분리(#80)
1 parent b8c0f84 commit 048fa2e

11 files changed

Lines changed: 278 additions & 24 deletions

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/controller/AnswerController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.wisoft.prepair.prepair_api.interview.answer.controller;
22

3-
import io.wisoft.prepair.prepair_api.interview.answer.dto.AnswerRequest;
4-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResponse;
3+
import io.wisoft.prepair.prepair_api.interview.answer.dto.request.AnswerRequest;
4+
import io.wisoft.prepair.prepair_api.interview.answer.dto.response.FeedbackResponse;
55
import io.wisoft.prepair.prepair_api.common.response.ApiResponse;
66
import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerService;
77
import io.wisoft.prepair.prepair_api.interview.answer.service.VideoAnswerSseService;

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AnalysisCompletedEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
import java.util.UUID;
44

5-
public record AllAnalysisCompletedEvent(UUID answerId, boolean hasFailed) {
5+
public record AnalysisCompletedEvent(UUID answerId, boolean hasFailed) {
66
}

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AnalysisCompletedHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
@Slf4j
1313
@Component
1414
@RequiredArgsConstructor
15-
public class AllAnalysisCompletedHandler {
15+
public class AnalysisCompletedHandler {
1616

1717
private final AnalysisCompletionService analysisCompletionService;
1818

1919
@Async("videoTaskExecutor")
2020
@EventListener
21-
public void handle(AllAnalysisCompletedEvent event) {
21+
public void handle(AnalysisCompletedEvent event) {
2222
UUID answerId = event.answerId();
2323

2424
if (event.hasFailed()) {

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AnalysisCompletionTracker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private void finishAnalysis(UUID answerId, boolean failed) {
5454
int analysisCount = state.analysisCount.incrementAndGet();
5555
if (analysisCount == ANALYSIS_TASKS) {
5656
eventPublisher.publishEvent(
57-
new AllAnalysisCompletedEvent(answerId, state.analysisFailed.get())
57+
new AnalysisCompletedEvent(answerId, state.analysisFailed.get())
5858
);
5959
}
6060
finishTask(answerId);

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/VideoAnswerProcessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import io.wisoft.prepair.prepair_api.common.exception.BusinessException;
44
import io.wisoft.prepair.prepair_api.common.exception.ErrorCode;
55
import io.wisoft.prepair.prepair_api.external.storage.S3FileStorage;
6-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackDetail;
7-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult;
6+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackDetail;
7+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackResult;
88
import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType;
99
import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerPersistenceService;
1010
import io.wisoft.prepair.prepair_api.interview.answer.service.FeedbackGenerator;
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package io.wisoft.prepair.prepair_api.interview.answer.service;
2+
3+
import io.wisoft.prepair.prepair_api.common.exception.BusinessException;
4+
import io.wisoft.prepair.prepair_api.common.exception.ErrorCode;
5+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.CombinedFeedbackResult;
6+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FinalFeedbackResult;
7+
import io.wisoft.prepair.prepair_api.interview.answer.dto.response.FinalFeedbackResponse;
8+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FinalFeedbackInput;
9+
import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType;
10+
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer;
11+
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback;
12+
import io.wisoft.prepair.prepair_api.interview.answer.repository.AnswerRepository;
13+
import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository;
14+
import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion;
15+
import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository;
16+
import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession;
17+
import io.wisoft.prepair.prepair_api.interview.session.notifier.SessionCompletionNotifier;
18+
import io.wisoft.prepair.prepair_api.interview.session.service.SessionPersistenceService;
19+
import lombok.RequiredArgsConstructor;
20+
import lombok.extern.slf4j.Slf4j;
21+
import org.springframework.stereotype.Service;
22+
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.UUID;
27+
import java.util.stream.Collectors;
28+
29+
@Slf4j
30+
@Service
31+
@RequiredArgsConstructor
32+
public class AnalysisCompletionService {
33+
34+
private final AnswerRepository answerRepository;
35+
private final FeedbackRepository feedbackRepository;
36+
private final QuestionRepository questionRepository;
37+
private final FeedbackGenerator feedbackGenerator;
38+
private final AnswerPersistenceService answerPersistenceService;
39+
private final SessionPersistenceService sessionPersistenceService;
40+
private final SessionCompletionNotifier completionNotifier;
41+
42+
public void processAnalysisCompletion(UUID answerId) {
43+
List<InterviewFeedback> feedbacks = feedbackRepository.findByInterviewAnswerId(answerId);
44+
45+
InterviewFeedback stt = findRequiredFeedback(feedbacks, FeedbackType.STT);
46+
InterviewFeedback video = findRequiredFeedback(feedbacks, FeedbackType.VIDEO);
47+
48+
InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId)
49+
.orElseThrow(() -> new BusinessException(ErrorCode.ANSWER_NOT_FOUND));
50+
51+
CombinedFeedbackResult result = feedbackGenerator.generateCombined(
52+
answer.getInterviewQuestion().getQuestion(),
53+
stt.getFeedback(),
54+
video.getFeedback()
55+
);
56+
57+
answerPersistenceService.saveCombinedFeedback(answerId, result);
58+
log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score());
59+
completeSessionIfReady(answer);
60+
}
61+
62+
private void completeSessionIfReady(InterviewAnswer answer) {
63+
InterviewSession session = getSession(answer);
64+
if (session == null) {
65+
log.warn("[최종평가] 세션 없음 - answerId: {}", answer.getId());
66+
return;
67+
}
68+
69+
if (!isFinalFeedbackReady(session)) {
70+
return;
71+
}
72+
73+
UUID sessionId = session.getId();
74+
FinalFeedbackData data = buildFinalData(sessionId);
75+
FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(data.toInputs());
76+
77+
completeSession(sessionId, data, finalResult);
78+
}
79+
80+
81+
public void failSession(UUID answerId, String message) {
82+
InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null);
83+
if (answer == null) {
84+
log.warn("[세션실패] 답변 없음 - answerId: {}", answerId);
85+
return;
86+
}
87+
88+
InterviewSession session = getSession(answer);
89+
if (session == null) {
90+
log.warn("[세션실패] 세션 없음 - answerId: {}", answerId);
91+
return;
92+
}
93+
94+
sessionPersistenceService.saveFailedSession(session.getId());
95+
completionNotifier.notifyFailure(session.getId(), message);
96+
}
97+
98+
private InterviewSession getSession(InterviewAnswer answer) {
99+
return answer.getInterviewQuestion().getInterviewSession();
100+
}
101+
102+
private InterviewFeedback findRequiredFeedback(List<InterviewFeedback> feedbacks, FeedbackType type) {
103+
return feedbacks.stream()
104+
.filter(f -> f.getFeedbackType() == type)
105+
.findFirst()
106+
.orElseThrow(() -> new BusinessException(ErrorCode.INTERNAL_ERROR));
107+
}
108+
109+
private boolean isFinalFeedbackReady(InterviewSession session) {
110+
UUID sessionId = session.getId();
111+
long combinedCount = feedbackRepository.countBySessionIdAndFeedbackType(sessionId, FeedbackType.COMBINED);
112+
113+
if (combinedCount < session.getTotalQuestionCount()) {
114+
log.info(
115+
"[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}",
116+
sessionId,
117+
combinedCount,
118+
session.getTotalQuestionCount()
119+
);
120+
return false;
121+
}
122+
return true;
123+
}
124+
125+
private void completeSession(
126+
UUID sessionId,
127+
FinalFeedbackData data,
128+
FinalFeedbackResult finalResult
129+
) {
130+
sessionPersistenceService.saveCompletedSession(sessionId, data.finalScore(), finalResult.finalFeedback());
131+
132+
FinalFeedbackResponse response = new FinalFeedbackResponse(
133+
sessionId,
134+
data.finalScore(),
135+
finalResult.finalFeedback(),
136+
data.questionFeedbacks()
137+
);
138+
139+
completionNotifier.notifyComplete(sessionId, response);
140+
141+
log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, data.finalScore());
142+
}
143+
144+
private FinalFeedbackData buildFinalData(UUID sessionId) {
145+
List<InterviewQuestion> questions = questionRepository.findByInterviewSessionId(sessionId);
146+
Map<UUID, InterviewAnswer> answerMap = getAnswerMap(sessionId);
147+
Map<UUID, List<InterviewFeedback>> feedbackMap = getFeedbackMap(sessionId);
148+
149+
List<FinalFeedbackResponse.QuestionFeedback> questionFeedbacks = new ArrayList<>();
150+
int totalScore = 0;
151+
152+
for (InterviewQuestion question : questions) {
153+
InterviewAnswer answer = answerMap.get(question.getId());
154+
if (answer == null) continue;
155+
156+
List<InterviewFeedback> answerFeedbacks = feedbackMap.getOrDefault(answer.getId(), List.of());
157+
InterviewFeedback combined = findOptionalFeedback(answerFeedbacks, FeedbackType.COMBINED);
158+
159+
if (combined == null) continue;
160+
161+
questionFeedbacks.add(new FinalFeedbackResponse.QuestionFeedback(
162+
question.getId(),
163+
question.getQuestion(),
164+
combined.getScore(),
165+
combined.getFeedback(),
166+
getFeedbackText(answerFeedbacks, FeedbackType.STT),
167+
getFeedbackText(answerFeedbacks, FeedbackType.VIDEO)
168+
));
169+
170+
totalScore += combined.getScore();
171+
}
172+
173+
int finalScore = calculateAverageScore(totalScore, questionFeedbacks.size());
174+
return new FinalFeedbackData(questionFeedbacks, finalScore);
175+
}
176+
177+
private Map<UUID, InterviewAnswer> getAnswerMap(UUID sessionId) {
178+
return answerRepository.findBySessionId(sessionId).stream()
179+
.collect(Collectors.toMap(a -> a.getInterviewQuestion().getId(), a -> a));
180+
}
181+
182+
private Map<UUID, List<InterviewFeedback>> getFeedbackMap(UUID sessionId) {
183+
return feedbackRepository.findAllBySessionId(sessionId).stream()
184+
.collect(Collectors.groupingBy(f -> f.getInterviewAnswer().getId()));
185+
}
186+
187+
private InterviewFeedback findOptionalFeedback(List<InterviewFeedback> feedbacks, FeedbackType type) {
188+
return feedbacks.stream()
189+
.filter(f -> f.getFeedbackType() == type)
190+
.findFirst()
191+
.orElse(null);
192+
}
193+
194+
private String getFeedbackText(List<InterviewFeedback> feedbacks, FeedbackType type) {
195+
InterviewFeedback feedback = findOptionalFeedback(feedbacks, type);
196+
return feedback == null ? null : feedback.getFeedback();
197+
}
198+
199+
private int calculateAverageScore(int totalScore, int count) {
200+
if (count == 0) {
201+
return 0;
202+
}
203+
204+
return totalScore / count;
205+
}
206+
207+
private record FinalFeedbackData(
208+
List<FinalFeedbackResponse.QuestionFeedback> questionFeedbacks,
209+
int finalScore
210+
) {
211+
List<FinalFeedbackInput> toInputs() {
212+
return questionFeedbacks.stream()
213+
.map(qf -> new FinalFeedbackInput(
214+
qf.questionId(),
215+
qf.question(),
216+
qf.combinedFeedback(),
217+
qf.combinedScore()
218+
))
219+
.toList();
220+
}
221+
}
222+
}

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerPersistenceService.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import io.wisoft.prepair.prepair_api.interview.answer.dto.AnswerSubmitResult;
6-
import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult;
7-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult;
8-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackDetail;
5+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.AnswerSubmitResult;
6+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.CombinedFeedbackResult;
7+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackResult;
8+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackDetail;
99
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer;
1010
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback;
1111
import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion;

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerService.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package io.wisoft.prepair.prepair_api.interview.answer.service;
22

3-
import io.wisoft.prepair.prepair_api.interview.answer.dto.AnswerSubmitResult;
4-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult;
5-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackDetail;
6-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResponse;
3+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.AnswerSubmitResult;
4+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackResult;
5+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackDetail;
6+
import io.wisoft.prepair.prepair_api.interview.answer.dto.response.FeedbackResponse;
77
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer;
88
import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion;
99
import io.wisoft.prepair.prepair_api.external.member.MemberServiceClient;

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FeedbackGenerator.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult;
6-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult;
7-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResult;
5+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.CombinedFeedbackResult;
6+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackResult;
7+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FinalFeedbackResult;
8+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FinalFeedbackInput;
89
import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion;
910
import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient;
1011
import io.wisoft.prepair.prepair_api.common.exception.BusinessException;
1112
import io.wisoft.prepair.prepair_api.common.exception.ErrorCode;
12-
import io.wisoft.prepair.prepair_api.interview.prompt.PromptBuilder;
13+
import io.wisoft.prepair.prepair_api.interview.prompt.FeedbackPromptBuilder;
1314
import lombok.RequiredArgsConstructor;
1415
import lombok.extern.slf4j.Slf4j;
1516
import org.springframework.stereotype.Component;
1617

18+
import java.util.List;
19+
1720
@Slf4j
1821
@Component
1922
@RequiredArgsConstructor
2023
public class FeedbackGenerator {
2124

22-
private final PromptBuilder promptBuilder;
25+
private final FeedbackPromptBuilder promptBuilder;
2326
private final ObjectMapper objectMapper;
2427
private final OpenAiClient openAiClient;
2528

@@ -51,8 +54,8 @@ public CombinedFeedbackResult generateCombined(final String question, final Stri
5154
}
5255
}
5356

54-
public FinalFeedbackResult generateFinal(final String questionsAndFeedbacks) {
55-
String prompt = promptBuilder.buildFinalFeedbackPrompt(questionsAndFeedbacks);
57+
public FinalFeedbackResult generateFinal(final List<FinalFeedbackInput> inputs) {
58+
String prompt = promptBuilder.buildFinalFeedbackPrompt(inputs);
5659
String raw = openAiClient.generateText(prompt);
5760

5861
try {

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoFrameAnalysisService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult;
5+
import io.wisoft.prepair.prepair_api.interview.answer.dto.internal.FeedbackResult;
66
import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient;
77
import io.wisoft.prepair.prepair_api.common.exception.BusinessException;
88
import io.wisoft.prepair.prepair_api.common.exception.ErrorCode;

0 commit comments

Comments
 (0)