From c198de1b9010b54bb7232299a47ca8f7c68114da Mon Sep 17 00:00:00 2001 From: jmj Date: Wed, 3 Jun 2026 13:45:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(feedback):=20=EC=A2=85=EB=A3=8C=EB=90=9C?= =?UTF-8?q?=20=EC=84=B8=EC=85=98=20=ED=95=9C=EC=A0=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=EC=9D=98=20expected=5Fsignal=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 면접 종료 후 피드백에서 "이 질문이 본 핵심(모범 답변 포인트)"을 보여주면 학습에 유용. 라이브 중엔 정답 유출 방지를 위해 계속 숨김. Core: - MessageResult/MessageResponse: expectedSignal 필드 추가 - InterviewMessageService.list: 세션이 종료 상태(COMPLETED/INTERRUPTED/ CANCELLED)일 때만 expectedSignal 채움. IN_PROGRESS/READY 면 null. - openapi 재생성 프론트: - QuestionBubble: expectedSignal 있으면 "이 질문이 본 핵심" 노트 표시. (백엔드가 라이브엔 null 로 내려 라이브 화면엔 안 보이고, 피드백 Q&A 전사에서만 노출) Co-Authored-By: Claude Opus 4.8 --- backend/openapi.json | 3 +++ .../application/InterviewMessageService.java | 13 +++++++++---- .../session/application/dto/MessageResult.java | 16 ++++++++++++---- .../presentation/dto/MessageResponse.java | 8 +++++--- .../interview/ui/live/QuestionBubble.tsx | 7 +++++++ frontend/src/shared/api/generated.ts | 1 + 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/backend/openapi.json b/backend/openapi.json index 48edb13b..9a6923ea 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -2679,6 +2679,9 @@ }, "audioFileUrl" : { "type" : "string" + }, + "expectedSignal" : { + "type" : "string" } } }, diff --git a/backend/src/main/java/com/stackup/stackup/session/application/InterviewMessageService.java b/backend/src/main/java/com/stackup/stackup/session/application/InterviewMessageService.java index 9100511a..5405421b 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/InterviewMessageService.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/InterviewMessageService.java @@ -39,19 +39,24 @@ public class InterviewMessageService { private final ApplicationEventPublisher events; public List list(Long userId, Long sessionId) { - ownedSession(userId, sessionId); + InterviewSession session = ownedSession(userId, sessionId); + // 종료된 세션에서만 expected_signal 노출(라이브/대기 중엔 정답 유출 방지). + boolean revealExpectedSignal = + session.getStatus() != SessionStatus.IN_PROGRESS + && session.getStatus() != SessionStatus.READY; return messageRepository.findBySession_IdOrderBySequenceNumberAsc(sessionId).stream() - .map(this::toResultWithAudioUrls) + .map(m -> toResultWithAudioUrls(m, revealExpectedSignal)) .toList(); } // 재생용 presigned URL 동봉: 질문 TTS(SUCCEEDED) + 음성 답변 원본. // presign 실패가 메시지 조회 전체를 깨뜨리지 않도록 개별 try/catch. - private MessageResult toResultWithAudioUrls(InterviewMessage m) { + private MessageResult toResultWithAudioUrls(InterviewMessage m, boolean revealExpectedSignal) { String ttsUrl = m.getTtsStatus() == TtsStatus.SUCCEEDED ? presign(m.getTtsAudioPath()) : null; String audioUrl = presign(m.getAudioFilePath()); - return MessageResult.of(m, ttsUrl, audioUrl); + String expectedSignal = revealExpectedSignal ? m.getExpectedSignal() : null; + return MessageResult.of(m, ttsUrl, audioUrl, expectedSignal); } private String presign(String key) { diff --git a/backend/src/main/java/com/stackup/stackup/session/application/dto/MessageResult.java b/backend/src/main/java/com/stackup/stackup/session/application/dto/MessageResult.java index 8a199e03..26b3fc97 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/dto/MessageResult.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/dto/MessageResult.java @@ -23,14 +23,21 @@ public record MessageResult( String targetEvidence, // 재생용 presigned URL. 조회(list) 경로에서만 채우고, 그 외에는 null. String ttsAudioUrl, - String audioFileUrl - // expectedSignal 은 의도적으로 제외 — 정답 유출 방지(라이브 비노출). + String audioFileUrl, + // 질문이 기대한 핵심(평가 관점). 라이브 중엔 null(정답 유출 방지), + // 종료된 세션 조회에서만 채워 피드백 학습용으로 노출. + String expectedSignal ) { public static MessageResult of(InterviewMessage m) { - return of(m, null, null); + return of(m, null, null, null); } public static MessageResult of(InterviewMessage m, String ttsAudioUrl, String audioFileUrl) { + return of(m, ttsAudioUrl, audioFileUrl, null); + } + + public static MessageResult of( + InterviewMessage m, String ttsAudioUrl, String audioFileUrl, String expectedSignal) { return new MessageResult( m.getId(), m.getSession().getId(), @@ -47,7 +54,8 @@ public static MessageResult of(InterviewMessage m, String ttsAudioUrl, String au m.getCategory(), m.getTargetEvidence(), ttsAudioUrl, - audioFileUrl + audioFileUrl, + expectedSignal ); } } diff --git a/backend/src/main/java/com/stackup/stackup/session/presentation/dto/MessageResponse.java b/backend/src/main/java/com/stackup/stackup/session/presentation/dto/MessageResponse.java index 67bbd03a..a8272f96 100644 --- a/backend/src/main/java/com/stackup/stackup/session/presentation/dto/MessageResponse.java +++ b/backend/src/main/java/com/stackup/stackup/session/presentation/dto/MessageResponse.java @@ -23,8 +23,9 @@ public record MessageResponse( String targetEvidence, // 재생용 presigned URL (질문 TTS / 음성 답변 원본). 조회 응답에서만 채움. String ttsAudioUrl, - String audioFileUrl - // expectedSignal 은 노출하지 않음 — 자기연습 시 정답 유출 방지. + String audioFileUrl, + // 질문이 기대한 핵심(평가 관점). 종료된 세션에서만 채워짐(라이브 중엔 null — 정답 유출 방지). + String expectedSignal ) { public static MessageResponse from(MessageResult r) { return new MessageResponse( @@ -43,7 +44,8 @@ public static MessageResponse from(MessageResult r) { r.category(), r.targetEvidence(), r.ttsAudioUrl(), - r.audioFileUrl() + r.audioFileUrl(), + r.expectedSignal() ); } } diff --git a/frontend/src/features/interview/ui/live/QuestionBubble.tsx b/frontend/src/features/interview/ui/live/QuestionBubble.tsx index 95bde1f2..9da2de6e 100644 --- a/frontend/src/features/interview/ui/live/QuestionBubble.tsx +++ b/frontend/src/features/interview/ui/live/QuestionBubble.tsx @@ -102,6 +102,13 @@ export function QuestionBubble({ )} + {/* expected_signal 은 종료된 세션에서만 내려옴(피드백 학습용). 라이브엔 비노출. */} + {message.expectedSignal && ( +
+ 이 질문이 본 핵심 ·{' '} + {message.expectedSignal} +
+ )} ) diff --git a/frontend/src/shared/api/generated.ts b/frontend/src/shared/api/generated.ts index f026805b..be41a2ee 100644 --- a/frontend/src/shared/api/generated.ts +++ b/frontend/src/shared/api/generated.ts @@ -867,6 +867,7 @@ export interface components { targetEvidence?: string; ttsAudioUrl?: string; audioFileUrl?: string; + expectedSignal?: string; }; VoiceStreamBeginResponse: { /** Format: int64 */