Skip to content

Commit 1664036

Browse files
committed
feat: 질문 콜백에서 첫 질문만 저장
1 parent 1d5154a commit 1664036

4 files changed

Lines changed: 93 additions & 13 deletions

File tree

backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
import org.springframework.stereotype.Service;
2222
import org.springframework.transaction.annotation.Transactional;
2323

24-
// callback.questions 처리.
25-
// POOL: 첫 질문을 interview_messages 에 INSERT + SSE session.message push.
26-
// (전체 풀은 first cut 에선 메모리상 처리 — 후속 PR 에서 별도 테이블 또는 컬럼)
27-
// FOLLOWUP: parent_message_id 매핑하여 INSERT + SSE push (B5 진행 시 활성)
24+
// callback.questions handling.
25+
// POOL is kept as the legacy callback kind, but Core treats it as the single
26+
// initial question result. Extra questions are ignored; Core does not manage a pool.
27+
// FOLLOWUP inserts a follow-up question and pushes it through SSE.
2828
@Service
2929
@RequiredArgsConstructor
3030
@Transactional(readOnly = true)
@@ -65,7 +65,7 @@ public void apply(QuestionsCallbackEnvelope envelope) {
6565
}
6666

6767
if (payload.isPool()) {
68-
applyPool(session, payload);
68+
applyInitialQuestion(session, payload);
6969
} else if (payload.isFollowup()) {
7070
applyFollowup(session, payload);
7171
} else {
@@ -74,13 +74,17 @@ public void apply(QuestionsCallbackEnvelope envelope) {
7474
markProcessed(envelope.messageId());
7575
}
7676

77-
private void applyPool(InterviewSession session, QuestionsCallbackPayload payload) {
77+
private void applyInitialQuestion(InterviewSession session, QuestionsCallbackPayload payload) {
7878
List<GeneratedQuestion> questions = payload.questions();
7979
if (questions == null || questions.isEmpty()) {
80-
log.warn("callback.questions POOL with no questions. sessionId={}", session.getId());
80+
log.warn("callback.questions initial result with no questions. sessionId={}", session.getId());
8181
return;
8282
}
8383
GeneratedQuestion first = questions.get(0);
84+
if (questions.size() > 1) {
85+
log.info("callback.questions initial result included extra questions; ignoring extras. sessionId={}, extra={}",
86+
session.getId(), questions.size() - 1);
87+
}
8488
InterviewMessage message = messageRepository.save(
8589
InterviewMessage.interviewer(session, 1, first.question())
8690
);
@@ -90,10 +94,10 @@ private void applyPool(InterviewSession session, QuestionsCallbackPayload payloa
9094
sseEventPublisher.publishToUser(
9195
session.getUser().getId(),
9296
SseEventType.SESSION_MESSAGE,
93-
new SessionMessageNotice(session.getId(), message.getId(), "QUESTION_POOL_READY")
97+
new SessionMessageNotice(session.getId(), message.getId(), "INITIAL_QUESTION_READY")
9498
);
95-
log.info("callback.questions POOL processed. sessionId={}, total={}",
96-
session.getId(), questions.size());
99+
log.info("callback.questions initial question processed. sessionId={}, ignored_extras={}",
100+
session.getId(), Math.max(questions.size() - 1, 0));
97101
}
98102

99103
private void applyFollowup(InterviewSession session, QuestionsCallbackPayload payload) {

backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
public record QuestionsCallbackPayload(
88
Long sessionId,
99
String kind, // POOL | FOLLOWUP
10-
List<GeneratedQuestion> questions, // POOL 시 사용
10+
List<GeneratedQuestion> questions, // POOL legacy kind: initial question result
1111
Long parentMessageId, // FOLLOWUP 시 사용
1212
String followupQuestion, // FOLLOWUP 시 사용
1313
AnswerEvaluation answerEvaluation // FOLLOWUP 시 옵션

backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.mockito.ArgumentMatchers.any;
55
import static org.mockito.ArgumentMatchers.eq;
6+
import static org.mockito.ArgumentMatchers.argThat;
67
import static org.mockito.Mockito.never;
78
import static org.mockito.Mockito.verify;
89
import static org.mockito.Mockito.when;
@@ -26,6 +27,7 @@
2627
import java.util.Optional;
2728
import org.junit.jupiter.api.Test;
2829
import org.junit.jupiter.api.extension.ExtendWith;
30+
import org.mockito.ArgumentCaptor;
2931
import org.mockito.InjectMocks;
3032
import org.mockito.Mock;
3133
import org.mockito.junit.jupiter.MockitoExtension;
@@ -42,7 +44,7 @@ class QuestionsCallbackServiceTest {
4244
@InjectMocks QuestionsCallbackService service;
4345

4446
@Test
45-
void apply_poolInsertsFirstQuestionAndPushesSse() {
47+
void apply_poolCallbackStoresOnlyInitialQuestionAndPushesSse() {
4648
InterviewSession session = sessionFixture(11L, SessionStatus.READY);
4749
QuestionsCallbackEnvelope env = poolEnvelope(11L, List.of(
4850
new GeneratedQuestion("INTRO", "Introduce yourself"),
@@ -58,8 +60,14 @@ void apply_poolInsertsFirstQuestionAndPushesSse() {
5860

5961
service.apply(env);
6062

61-
verify(messageRepository).save(any(InterviewMessage.class));
63+
ArgumentCaptor<InterviewMessage> messageCaptor = ArgumentCaptor.forClass(InterviewMessage.class);
64+
verify(messageRepository).save(messageCaptor.capture());
65+
assertThat(messageCaptor.getValue().getContent()).isEqualTo("Introduce yourself");
66+
assertThat(messageCaptor.getValue().getSequenceNumber()).isEqualTo(1);
6267
verify(sseEventPublisher).publishToSession(eq(11L), eq(SseEventType.SESSION_MESSAGE), any());
68+
verify(sseEventPublisher).publishToUser(eq(1L), eq(SseEventType.SESSION_MESSAGE),
69+
argThat(payload -> payload instanceof QuestionsCallbackService.SessionMessageNotice notice
70+
&& "INITIAL_QUESTION_READY".equals(notice.reason())));
6371
verify(processedMessageRepository).save(any(ProcessedMessage.class));
6472
assertThat(session.getTotalQuestionCount()).isEqualTo(1);
6573
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.stackup.stackup.session.application;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.ArgumentMatchers.eq;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.when;
8+
9+
import com.stackup.stackup.common.config.properties.RabbitMqProperties;
10+
import com.stackup.stackup.common.messaging.MessageContext;
11+
import com.stackup.stackup.common.messaging.RabbitMessagePublisher;
12+
import com.stackup.stackup.common.storage.ObjectStorageClient;
13+
import com.stackup.stackup.document.domain.AnalyzedDocumentRepository;
14+
import com.stackup.stackup.session.application.dto.GenerateQuestionsPayload;
15+
import com.stackup.stackup.session.application.event.SessionCreatedEvent;
16+
import com.stackup.stackup.session.domain.JobCategory;
17+
import com.stackup.stackup.session.domain.SessionMode;
18+
import java.util.List;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
import org.mockito.ArgumentCaptor;
22+
import org.mockito.InjectMocks;
23+
import org.mockito.Mock;
24+
import org.mockito.junit.jupiter.MockitoExtension;
25+
26+
@ExtendWith(MockitoExtension.class)
27+
class SessionQuestionsRequesterTest {
28+
29+
@Mock RabbitMessagePublisher publisher;
30+
@Mock RabbitMqProperties properties;
31+
@Mock AnalyzedDocumentRepository documentRepository;
32+
@Mock ObjectStorageClient storage;
33+
@InjectMocks SessionQuestionsRequester requester;
34+
35+
@Test
36+
void onSessionCreated_requestsOneInitialQuestionAndKeepsSessionLimit() {
37+
when(properties.routingKeys()).thenReturn(mockRoutingKeys());
38+
39+
requester.onSessionCreated(new SessionCreatedEvent(
40+
1L,
41+
11L,
42+
SessionMode.INTEGRATED,
43+
JobCategory.BACKEND,
44+
5,
45+
List.of()
46+
));
47+
48+
ArgumentCaptor<GenerateQuestionsPayload> payloadCaptor =
49+
ArgumentCaptor.forClass(GenerateQuestionsPayload.class);
50+
verify(publisher).publishToAi(eq("generate.questions"), payloadCaptor.capture(), any(MessageContext.class));
51+
52+
GenerateQuestionsPayload payload = payloadCaptor.getValue();
53+
assertThat(payload.sessionId()).isEqualTo(11L);
54+
assertThat(payload.mode()).isEqualTo(SessionMode.INTEGRATED);
55+
assertThat(payload.jobCategory()).isEqualTo(JobCategory.BACKEND);
56+
assertThat(payload.documents()).isEmpty();
57+
assertThat(payload.initialQuestionCount()).isEqualTo(1);
58+
assertThat(payload.maxQuestions()).isEqualTo(5);
59+
}
60+
61+
private RabbitMqProperties.RoutingKeyProperties mockRoutingKeys() {
62+
return new RabbitMqProperties.RoutingKeyProperties(
63+
"analyze.resume", "analyze.repository",
64+
"generate.questions", "generate.followup", "generate.feedback", "analyze.voice",
65+
"callback.analysis", "callback.questions", "callback.feedback", "callback.voice",
66+
"realtime.session.notify");
67+
}
68+
}

0 commit comments

Comments
 (0)