11package com .stackup .stackup .session .application ;
22
3+ import com .fasterxml .jackson .core .type .TypeReference ;
4+ import com .fasterxml .jackson .databind .json .JsonMapper ;
35import com .stackup .stackup .common .exception .ApiErrorCode ;
46import com .stackup .stackup .common .exception .DomainException ;
57import com .stackup .stackup .common .storage .ObjectStorageClient ;
1113import com .stackup .stackup .session .domain .InterviewSessionRepository ;
1214import com .stackup .stackup .session .domain .MessageRole ;
1315import com .stackup .stackup .session .domain .MessageStatus ;
16+ import com .stackup .stackup .session .domain .MessageVoiceAnalysis ;
17+ import com .stackup .stackup .session .domain .MessageVoiceAnalysisRepository ;
1418import com .stackup .stackup .session .domain .SessionStatus ;
1519import com .stackup .stackup .session .domain .TtsStatus ;
1620import java .net .URI ;
1721import java .time .Duration ;
1822import java .util .List ;
23+ import java .util .Map ;
1924import lombok .RequiredArgsConstructor ;
2025import org .slf4j .Logger ;
2126import org .slf4j .LoggerFactory ;
@@ -33,29 +38,55 @@ public class InterviewMessageService {
3338 private static final Logger log = LoggerFactory .getLogger (InterviewMessageService .class );
3439 private static final Duration AUDIO_URL_TTL = Duration .ofMinutes (30 );
3540
41+ private static final JsonMapper JSON = JsonMapper .builder ().build ();
42+ private static final TypeReference <Map <String , Integer >> FILLER_TYPE = new TypeReference <>() {};
43+
3644 private final InterviewSessionRepository sessionRepository ;
3745 private final InterviewMessageRepository messageRepository ;
46+ private final MessageVoiceAnalysisRepository voiceAnalysisRepository ;
3847 private final ObjectStorageClient storage ;
3948 private final ApplicationEventPublisher events ;
4049
4150 public List <MessageResult > list (Long userId , Long sessionId ) {
4251 InterviewSession session = ownedSession (userId , sessionId );
43- // 종료된 세션에서만 expected_signal 노출(라이브/대기 중엔 정답 유출 방지).
44- boolean revealExpectedSignal =
52+ // 종료된 세션에서만 평가·복기·전달력 노출(라이브/대기 중엔 정답 유출 방지).
53+ boolean revealInsights =
4554 session .getStatus () != SessionStatus .IN_PROGRESS
4655 && session .getStatus () != SessionStatus .READY ;
56+ // 답변별 음성 분석(전달력 메트릭) — 메시지 id 로 매핑. 종료 세션에서만 조회.
57+ Map <Long , MessageVoiceAnalysis > voiceByMessageId = revealInsights
58+ ? voiceAnalysisRepository .findByMessage_Session_Id (sessionId ).stream ()
59+ .collect (java .util .stream .Collectors .toMap (
60+ v -> v .getMessage ().getId (), v -> v , (a , b ) -> a ))
61+ : Map .of ();
4762 return messageRepository .findBySession_IdOrderBySequenceNumberAsc (sessionId ).stream ()
48- .map (m -> toResultWithAudioUrls (m , revealExpectedSignal ))
63+ .map (m -> toResultWithAudioUrls (m , revealInsights , voiceByMessageId . get ( m . getId ()) ))
4964 .toList ();
5065 }
5166
5267 // 재생용 presigned URL 동봉: 질문 TTS(SUCCEEDED) + 음성 답변 원본.
5368 // presign 실패가 메시지 조회 전체를 깨뜨리지 않도록 개별 try/catch.
54- private MessageResult toResultWithAudioUrls (InterviewMessage m , boolean revealInsights ) {
69+ private MessageResult toResultWithAudioUrls (
70+ InterviewMessage m , boolean revealInsights , MessageVoiceAnalysis voice ) {
5571 String ttsUrl = m .getTtsStatus () == TtsStatus .SUCCEEDED
5672 ? presign (m .getTtsAudioPath ()) : null ;
5773 String audioUrl = presign (m .getAudioFilePath ());
58- return MessageResult .of (m , ttsUrl , audioUrl , revealInsights );
74+ Double wpm = voice == null ? null : voice .getSpeakingRateWpm ();
75+ Double silence = voice == null ? null : voice .getSilenceDurationSec ();
76+ Map <String , Integer > fillers = voice == null ? null : parseFillers (voice .getFillerWordCounts ());
77+ return MessageResult .of (m , ttsUrl , audioUrl , revealInsights , wpm , silence , fillers );
78+ }
79+
80+ private Map <String , Integer > parseFillers (String json ) {
81+ if (json == null || json .isBlank ()) {
82+ return null ;
83+ }
84+ try {
85+ return JSON .readValue (json , FILLER_TYPE );
86+ } catch (com .fasterxml .jackson .core .JsonProcessingException e ) {
87+ log .warn ("filler_word_counts parse failed" , e );
88+ return null ;
89+ }
5990 }
6091
6192 private String presign (String key ) {
0 commit comments