diff --git a/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java b/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java index dc1f42ce..5519a759 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java @@ -129,7 +129,8 @@ private void insertGeneralFromPool(InterviewSession session, SessionQuestionPool pool.getCategory(), pool.getTargetEvidence(), pool.getExpectedSignal())); session.incrementQuestionCount(); publishQuestionEvents(session, message, reason); - maybeAutoEnd(session); + // 여기서 자동종료하지 않는다 — 방금 던진 메인질문은 답변·꼬리질문을 거쳐야 한다. + // maxQuestions 도달 종료는 꼬리 사이클 후 advanceToNextGeneral 에서 판정한다. } private void publishQuestionEvents(InterviewSession session, InterviewMessage message, String reason) { @@ -140,12 +141,6 @@ private void publishQuestionEvents(InterviewSession session, InterviewMessage me new SessionMessageNotice(session.getId(), message.getId(), reason))); } - private void maybeAutoEnd(InterviewSession session) { - if (session.isMaxReached()) { - endSession(session, "MAX_QUESTIONS_REACHED"); - } - } - private void endSession(InterviewSession session, String reason) { try { session.end(); @@ -205,13 +200,10 @@ private void applyFollowup(InterviewSession session, QuestionsCallbackPayload pa : InterviewMessage.followup(session, nextSeq, payload.followupQuestion(), parent)); } - if (!clarification) { - session.incrementQuestionCount(); - } + // 꼬리질문은 maxQuestions(메인질문 수) 카운트에 포함하지 않고, 여기서 자동종료하지도 + // 않는다 — 종료는 메인질문의 꼬리 사이클이 끝나고 advanceToNextGeneral 시점에서만 일어나야 + // 답변 직후 생성된 꼬리질문을 버리고 갑자기 피드백으로 튕기는 일이 없다. publishQuestionEvents(session, message, clarification ? "CLARIFICATION" : "FOLLOWUP_READY"); - if (!clarification) { - maybeAutoEnd(session); - } log.info("callback.questions FOLLOWUP processed. sessionId={}, msg={}, clarification={}", session.getId(), message.getId(), clarification); } diff --git a/backend/src/main/java/com/stackup/stackup/session/domain/InterviewSession.java b/backend/src/main/java/com/stackup/stackup/session/domain/InterviewSession.java index a293ebc6..3ce80676 100644 --- a/backend/src/main/java/com/stackup/stackup/session/domain/InterviewSession.java +++ b/backend/src/main/java/com/stackup/stackup/session/domain/InterviewSession.java @@ -179,6 +179,8 @@ public void cancel() { this.status = SessionStatus.CANCELLED; } + // 메인(일반) 질문만 센다. 꼬리질문은 maxQuestions 한도에 포함하지 않는다 + // (꼬리질문은 maxFollowupsPerQuestion 으로 별도 제한). public void incrementQuestionCount() { if (totalQuestionCount == null) { totalQuestionCount = 0; @@ -186,6 +188,7 @@ public void incrementQuestionCount() { totalQuestionCount++; } + // 던진 메인질문 수가 maxQuestions 에 도달했는지. 꼬리 사이클 종료 후 advanceToNextGeneral 에서 판정. public boolean isMaxReached() { return totalQuestionCount != null && maxQuestions != null diff --git a/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java b/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java index fb0bfcfc..cdcf9699 100644 --- a/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java +++ b/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java @@ -96,9 +96,10 @@ void apply_poolCallbackSeedsPoolAndInsertsFirstQuestion() { } @Test - void apply_followupAutoEndsSessionAtMaxQuestions() { + void apply_followupDoesNotCountOrAutoEnd() { InterviewSession session = sessionFixture(11L, SessionStatus.IN_PROGRESS); - // maxQuestions=5; total=4, then one follow-up reaches the limit. + // 꼬리질문은 maxQuestions(메인질문 수) 한도에 포함되지 않으며, 도착해도 세션을 + // 끝내지 않는다(종료는 메인질문의 꼬리 사이클 후 advanceToNextGeneral 에서만). ReflectionTestUtils.setField(session, "totalQuestionCount", 4); QuestionsCallbackEnvelope env = followupEnvelope(11L, 200L, "Follow-up?"); @@ -114,8 +115,8 @@ void apply_followupAutoEndsSessionAtMaxQuestions() { service.apply(env); - assertThat(session.getStatus()).isEqualTo(SessionStatus.COMPLETED); - assertThat(session.getTotalQuestionCount()).isEqualTo(5); + assertThat(session.getStatus()).isEqualTo(SessionStatus.IN_PROGRESS); + assertThat(session.getTotalQuestionCount()).isEqualTo(4); } @Test @@ -188,7 +189,7 @@ void apply_clarificationReExplainsWithoutCountingQuestion() { // ── 새 placeholder 경로 테스트 ───────────────────────────────────────────── @Test - void apply_followupNormal_updatesPlaceholderInPlaceAndCounts() { + void apply_followupNormal_updatesPlaceholderInPlaceWithoutCounting() { InterviewSession session = sessionFixture(20L, SessionStatus.IN_PROGRESS); // placeholder: followupPlaceholder 의 sentinel content InterviewMessage placeholder = InterviewMessage.followupPlaceholder( @@ -213,8 +214,8 @@ void apply_followupNormal_updatesPlaceholderInPlaceAndCounts() { // placeholder 가 in-place 로 업데이트되어야 함 assertThat(placeholder.getContent()).isEqualTo("꼬리질문 내용?"); assertThat(placeholder.getStatus()).isEqualTo(com.stackup.stackup.session.domain.MessageStatus.COMPLETED); - // 질문 수 카운트 증가 - assertThat(session.getTotalQuestionCount()).isEqualTo(1); + // 꼬리질문은 메인질문 카운트(maxQuestions 한도)에 포함되지 않는다. + assertThat(session.getTotalQuestionCount()).isEqualTo(0); // save 는 placeholder 자체에 대해 호출됨 (새 InterviewMessage INSERT 아님) ArgumentCaptor cap = ArgumentCaptor.forClass(InterviewMessage.class); verify(messageRepository).save(cap.capture()); @@ -281,6 +282,19 @@ void apply_followupDontKnow_deletesPlaceholderAndAdvances() { assertThat(session.getStatus()).isEqualTo(SessionStatus.COMPLETED); } + @Test + void advanceToNextGeneral_endsWithMaxReached_whenMainQuestionsHitLimit() { + InterviewSession session = sessionFixture(33L, SessionStatus.IN_PROGRESS); + // maxQuestions=5; 메인질문 5개를 이미 던진 상태 → 다음 advance 에서 풀을 보지 않고 종료. + ReflectionTestUtils.setField(session, "totalQuestionCount", 5); + when(sessionRepository.findById(33L)).thenReturn(Optional.of(session)); + + service.advanceToNextGeneral(33L); + + assertThat(session.getStatus()).isEqualTo(SessionStatus.COMPLETED); + verify(poolRepository, never()).findFirstBySessionIdAndUsedFalseOrderByIdxAsc(any()); + } + private QuestionsCallbackEnvelope poolEnvelope(Long sessionId, List questions) { QuestionsCallbackPayload payload = new QuestionsCallbackPayload( sessionId, "POOL", questions, null, null, null, null, null, null