From 5439874bbb85b7a0676594c6ceb03b8a8642194d Mon Sep 17 00:00:00 2001 From: Boyeon-Shin Date: Mon, 1 Jun 2026 19:28:09 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=ED=99=94=EC=83=81=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=91=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AnalysisCompletionService.java | 65 ++------ .../session/controller/SessionController.java | 40 +++++ .../dto/response/SessionDetailResponse.java | 24 +++ .../session/dto/response/SessionResponse.java | 25 +++ .../session/repository/SessionRepository.java | 2 + .../service/SessionPersistenceService.java | 41 ----- .../session/service/SessionService.java | 143 ++++++++++++++++++ 7 files changed, 249 insertions(+), 91 deletions(-) create mode 100644 src/main/java/io/wisoft/prepair/prepair_api/interview/session/controller/SessionController.java create mode 100644 src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java create mode 100644 src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionResponse.java delete mode 100644 src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionPersistenceService.java create mode 100644 src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnalysisCompletionService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnalysisCompletionService.java index 5369a30..9deee17 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnalysisCompletionService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnalysisCompletionService.java @@ -11,20 +11,17 @@ import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; 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.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; +import io.wisoft.prepair.prepair_api.interview.session.dto.response.SessionDetailResponse; import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; import io.wisoft.prepair.prepair_api.interview.session.notifier.SessionCompletionNotifier; -import io.wisoft.prepair.prepair_api.interview.session.service.SessionPersistenceService; +import io.wisoft.prepair.prepair_api.interview.session.service.SessionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; @Slf4j @Service @@ -33,10 +30,9 @@ public class AnalysisCompletionService { private final AnswerRepository answerRepository; private final FeedbackRepository feedbackRepository; - private final QuestionRepository questionRepository; private final FeedbackGenerator feedbackGenerator; private final AnswerPersistenceService answerPersistenceService; - private final SessionPersistenceService sessionPersistenceService; + private final SessionService sessionService; private final SessionCompletionNotifier completionNotifier; public void processAnalysisCompletion(UUID answerId) { @@ -91,7 +87,7 @@ public void failSession(UUID answerId, String message) { return; } - sessionPersistenceService.saveFailedSession(session.getId()); + sessionService.saveFailedSession(session.getId()); completionNotifier.notifyFailure(session.getId(), message); } @@ -127,7 +123,7 @@ private void completeSession( FinalFeedbackData data, FinalFeedbackResult finalResult ) { - sessionPersistenceService.saveCompletedSession(sessionId, data.finalScore(), finalResult.finalFeedback()); + sessionService.saveCompletedSession(sessionId, data.finalScore(), finalResult.finalFeedback()); FinalFeedbackResponse response = new FinalFeedbackResponse( sessionId, @@ -142,60 +138,29 @@ private void completeSession( } private FinalFeedbackData buildFinalData(UUID sessionId) { - List questions = questionRepository.findByInterviewSessionId(sessionId); - Map answerMap = getAnswerMap(sessionId); - Map> feedbackMap = getFeedbackMap(sessionId); + List aggregated = sessionService.buildQuestionFeedbacks(sessionId); List questionFeedbacks = new ArrayList<>(); int totalScore = 0; - for (InterviewQuestion question : questions) { - InterviewAnswer answer = answerMap.get(question.getId()); - if (answer == null) continue; - - List answerFeedbacks = feedbackMap.getOrDefault(answer.getId(), List.of()); - InterviewFeedback combined = findOptionalFeedback(answerFeedbacks, FeedbackType.COMBINED); - - if (combined == null) continue; + for (SessionDetailResponse.QuestionFeedback qf : aggregated) { + if (qf.combinedScore() == null) continue; questionFeedbacks.add(new FinalFeedbackResponse.QuestionFeedback( - question.getId(), - question.getQuestion(), - combined.getScore(), - combined.getFeedback(), - getFeedbackText(answerFeedbacks, FeedbackType.STT), - getFeedbackText(answerFeedbacks, FeedbackType.VIDEO) + qf.questionId(), + qf.question(), + qf.combinedScore(), + qf.combinedFeedback(), + qf.sttFeedback(), + qf.videoFeedback() )); - - totalScore += combined.getScore(); + totalScore += qf.combinedScore(); } int finalScore = calculateAverageScore(totalScore, questionFeedbacks.size()); return new FinalFeedbackData(questionFeedbacks, finalScore); } - private Map getAnswerMap(UUID sessionId) { - return answerRepository.findBySessionId(sessionId).stream() - .collect(Collectors.toMap(a -> a.getInterviewQuestion().getId(), a -> a)); - } - - private Map> getFeedbackMap(UUID sessionId) { - return feedbackRepository.findAllBySessionId(sessionId).stream() - .collect(Collectors.groupingBy(f -> f.getInterviewAnswer().getId())); - } - - private InterviewFeedback findOptionalFeedback(List feedbacks, FeedbackType type) { - return feedbacks.stream() - .filter(f -> f.getFeedbackType() == type) - .findFirst() - .orElse(null); - } - - private String getFeedbackText(List feedbacks, FeedbackType type) { - InterviewFeedback feedback = findOptionalFeedback(feedbacks, type); - return feedback == null ? null : feedback.getFeedback(); - } - private int calculateAverageScore(int totalScore, int count) { if (count == 0) { return 0; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/controller/SessionController.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/controller/SessionController.java new file mode 100644 index 0000000..6c01e58 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/controller/SessionController.java @@ -0,0 +1,40 @@ +package io.wisoft.prepair.prepair_api.interview.session.controller; + +import io.wisoft.prepair.prepair_api.common.response.ApiResponse; +import io.wisoft.prepair.prepair_api.interview.session.dto.response.SessionDetailResponse; +import io.wisoft.prepair.prepair_api.interview.session.dto.response.SessionResponse; +import io.wisoft.prepair.prepair_api.interview.session.service.SessionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/interviews/sessions") +public class SessionController { + + private final SessionService sessionService; + + @GetMapping + public ApiResponse> getSessions( + @RequestHeader("X-User-Id") UUID memberId + ) { + List data = sessionService.getSessionsByMember(memberId); + return ApiResponse.ok(data, "세션 목록을 조회했습니다."); + } + + @GetMapping("/{sessionId}") + public ApiResponse getSession( + @PathVariable UUID sessionId, + @RequestHeader("X-User-Id") UUID memberId + ) { + SessionDetailResponse data = sessionService.getSessionDetail(sessionId, memberId); + return ApiResponse.ok(data, "세션 결과를 조회했습니다."); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java new file mode 100644 index 0000000..e416ebe --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java @@ -0,0 +1,24 @@ +package io.wisoft.prepair.prepair_api.interview.session.dto.response; + +import io.wisoft.prepair.prepair_api.interview.session.entity.SessionStatus; + +import java.util.List; +import java.util.UUID; + +public record SessionDetailResponse( + UUID sessionId, + SessionStatus status, + Integer finalScore, + String finalFeedback, + List questions +) { + public record QuestionFeedback( + UUID questionId, + String question, + Integer combinedScore, + String combinedFeedback, + String sttFeedback, + String videoFeedback + ) { + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionResponse.java new file mode 100644 index 0000000..1023ca0 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionResponse.java @@ -0,0 +1,25 @@ +package io.wisoft.prepair.prepair_api.interview.session.dto.response; + +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.session.entity.SessionStatus; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record SessionResponse( + UUID sessionId, + SessionStatus status, + Integer finalScore, + int totalQuestionCount, + LocalDateTime createdAt +) { + public static SessionResponse from(InterviewSession session) { + return new SessionResponse( + session.getId(), + session.getStatus(), + session.getFinalScore(), + session.getTotalQuestionCount(), + session.getCreatedAt() + ); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java index c925314..148a790 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java @@ -1,8 +1,10 @@ package io.wisoft.prepair.prepair_api.interview.session.repository; import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import java.util.List; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; public interface SessionRepository extends JpaRepository { + List findByMemberIdOrderByCreatedAtDesc(UUID memberId); } \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionPersistenceService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionPersistenceService.java deleted file mode 100644 index ef64a42..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionPersistenceService.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.wisoft.prepair.prepair_api.interview.session.service; - -import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; -import io.wisoft.prepair.prepair_api.interview.session.entity.SessionStatus; -import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class SessionPersistenceService { - - private final SessionRepository sessionRepository; - - @Transactional - public void saveCompletedSession(UUID sessionId, int finalScore, String finalFeedback) { - InterviewSession session = sessionRepository.findById(sessionId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 세션입니다.")); - - if (session.getStatus() == SessionStatus.COMPLETED) { - return; - } - - session.complete(finalScore, finalFeedback); - } - - @Transactional - public void saveFailedSession(UUID sessionId) { - InterviewSession session = sessionRepository.findById(sessionId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 세션입니다.")); - - if (session.getStatus() != SessionStatus.IN_PROGRESS) { - return; - } - - session.fail(); - } -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java new file mode 100644 index 0000000..3f41e86 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java @@ -0,0 +1,143 @@ +package io.wisoft.prepair.prepair_api.interview.session.service; + +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; +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.answer.repository.AnswerRepository; +import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository; +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.dto.response.SessionDetailResponse; +import io.wisoft.prepair.prepair_api.interview.session.dto.response.SessionResponse; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.session.entity.SessionStatus; +import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class SessionService { + + private final SessionRepository sessionRepository; + private final QuestionRepository questionRepository; + private final AnswerRepository answerRepository; + private final FeedbackRepository feedbackRepository; + + public List getSessionsByMember(UUID memberId) { + return sessionRepository.findByMemberIdOrderByCreatedAtDesc(memberId).stream() + .map(SessionResponse::from) + .toList(); + } + + public SessionDetailResponse getSessionDetail(UUID sessionId, UUID memberId) { + InterviewSession session = sessionRepository.findById(sessionId) + .orElseThrow(() -> new BusinessException(ErrorCode.SESSION_NOT_FOUND)); + + if (!session.getMemberId().equals(memberId)) { + throw new BusinessException(ErrorCode.FORBIDDEN); + } + + List questions = buildQuestionFeedbacks(sessionId); + + return new SessionDetailResponse( + sessionId, + session.getStatus(), + session.getFinalScore(), + session.getFinalFeedback(), + questions + ); + } + + public List buildQuestionFeedbacks(UUID sessionId) { + List questions = questionRepository.findByInterviewSessionId(sessionId); + Map answerMap = getAnswerMap(sessionId); + Map> feedbackMap = getFeedbackMap(sessionId); + + List result = new ArrayList<>(); + for (InterviewQuestion question : questions) { + InterviewAnswer answer = answerMap.get(question.getId()); + if (answer == null) { + result.add(new SessionDetailResponse.QuestionFeedback( + question.getId(), question.getQuestion(), + null, null, null, null + )); + continue; + } + + List answerFeedbacks = feedbackMap.getOrDefault(answer.getId(), List.of()); + InterviewFeedback combined = findOptionalFeedback(answerFeedbacks, FeedbackType.COMBINED); + + result.add(new SessionDetailResponse.QuestionFeedback( + question.getId(), + question.getQuestion(), + combined == null ? null : combined.getScore(), + combined == null ? null : combined.getFeedback(), + getFeedbackText(answerFeedbacks, FeedbackType.STT), + getFeedbackText(answerFeedbacks, FeedbackType.VIDEO) + )); + } + return result; + } + + @Transactional + public void saveCompletedSession(UUID sessionId, int finalScore, String finalFeedback) { + InterviewSession session = sessionRepository.findById(sessionId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 세션입니다.")); + + if (session.getStatus() == SessionStatus.COMPLETED) { + return; + } + + session.complete(finalScore, finalFeedback); + } + + @Transactional + public void saveFailedSession(UUID sessionId) { + InterviewSession session = sessionRepository.findById(sessionId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 세션입니다.")); + + if (session.getStatus() != SessionStatus.IN_PROGRESS) { + return; + } + + session.fail(); + } + + private Map getAnswerMap(UUID sessionId) { + return answerRepository.findBySessionId(sessionId).stream() + .collect(Collectors.toMap( + a -> a.getInterviewQuestion().getId(), + a -> a, + (existing, replacement) -> + existing.getCreatedAt().isAfter(replacement.getCreatedAt()) ? existing : replacement + )); + } + + private Map> getFeedbackMap(UUID sessionId) { + return feedbackRepository.findAllBySessionId(sessionId).stream() + .collect(Collectors.groupingBy(f -> f.getInterviewAnswer().getId())); + } + + private InterviewFeedback findOptionalFeedback(List feedbacks, FeedbackType type) { + return feedbacks.stream() + .filter(f -> f.getFeedbackType() == type) + .findFirst() + .orElse(null); + } + + private String getFeedbackText(List feedbacks, FeedbackType type) { + InterviewFeedback feedback = findOptionalFeedback(feedbacks, type); + return feedback == null ? null : feedback.getFeedback(); + } +} From 795efb38d11014a82436f4bae5cc5af38a7f518c Mon Sep 17 00:00:00 2001 From: Boyeon-Shin Date: Mon, 1 Jun 2026 19:28:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=ED=99=94=EC=83=81=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=91=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interview/session/dto/response/SessionDetailResponse.java | 1 + .../prepair_api/interview/session/service/SessionService.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java index e416ebe..df8107e 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/dto/response/SessionDetailResponse.java @@ -15,6 +15,7 @@ public record SessionDetailResponse( public record QuestionFeedback( UUID questionId, String question, + String mediaUrl, Integer combinedScore, String combinedFeedback, String sttFeedback, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java index 3f41e86..ab938ff 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/service/SessionService.java @@ -70,7 +70,7 @@ public List buildQuestionFeedbacks(UUID if (answer == null) { result.add(new SessionDetailResponse.QuestionFeedback( question.getId(), question.getQuestion(), - null, null, null, null + null, null, null, null, null )); continue; } @@ -81,6 +81,7 @@ public List buildQuestionFeedbacks(UUID result.add(new SessionDetailResponse.QuestionFeedback( question.getId(), question.getQuestion(), + answer.getMediaUrl(), combined == null ? null : combined.getScore(), combined == null ? null : combined.getFeedback(), getFeedbackText(answerFeedbacks, FeedbackType.STT),