Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.devkor.ifive.nadab.domain.askchat.api.dto.request.AskChatSessionStartRequest;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatHomeResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatQuestionSendResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatRemainingMessageCountResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatTurnChargeResponse;
import com.devkor.ifive.nadab.domain.askchat.application.AskChatMessageCommandService;
import com.devkor.ifive.nadab.domain.askchat.application.AskChatSessionService;
Expand Down Expand Up @@ -158,6 +159,37 @@ public ResponseEntity<ApiResponseDto<AskChatTurnChargeResponse>> chargeTurns(
return ApiResponseEntity.ok(response);
}

@GetMapping("/turns/remaining")
@PreAuthorize("isAuthenticated()")
@Operation(
summary = "물어보기 남은 메시지 횟수 조회",
description = """
현재 사용자가 사용할 수 있는 물어보기 남은 메시지 횟수만 조회합니다. </br>
홈 전체 정보를 다시 조회하지 않고 질문 전송/충전 이후 카운터만 갱신할 때 사용할 수 있습니다.
""",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(
responseCode = "200",
description = "남은 메시지 횟수 조회 성공",
content = @Content(schema = @Schema(implementation = AskChatRemainingMessageCountResponse.class))
),
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
@ApiResponse(
responseCode = "404",
description = "- ErrorCode: ASK_CHAT_WALLET_NOT_FOUND - Ask Chat 대화권 지갑을 찾을 수 없음",
content = @Content
)
}
)
public ResponseEntity<ApiResponseDto<AskChatRemainingMessageCountResponse>> getRemainingTurns(
@AuthenticationPrincipal UserPrincipal principal
) {
AskChatRemainingMessageCountResponse response =
askChatSessionService.getRemainingMessageCount(principal.getId());
return ApiResponseEntity.ok(response);
}

@PostMapping("/messages")
@PreAuthorize("isAuthenticated()")
@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public record AskChatQuestionSendResponse(
@Schema(description = "답변 생성 성공/실패 상태. 실패 시 프론트에서는 이 값을 기준으로 모달/토스트를 표시합니다.")
AskChatAnswerGenerationResponse answerGeneration,

@Schema(description = "답변 생성 처리 이후 사용 가능한 남은 메시지 횟수. 성공 시에는 차감 이후 값, 실패 시에는 환불 이후 값입니다.", example = "8")
int remainingMessageCount,

@Schema(description = "AI가 제안한 후속 추천 질문. 생성 실패 시 빈 배열")
List<String> followUpQuestions
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.devkor.ifive.nadab.domain.askchat.api.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "물어보기 남은 메시지 횟수 응답")
public record AskChatRemainingMessageCountResponse(
@Schema(description = "사용 가능한 남은 메시지 횟수", example = "9")
int remainingMessageCount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatMessageRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatRagDocumentRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatSessionRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatWalletRepository;
import com.devkor.ifive.nadab.domain.askchat.infra.AskChatAnswerLlmClient;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.BadRequestException;
Expand Down Expand Up @@ -48,6 +49,7 @@ public class AskChatMessageCommandService {
private final AskChatAnswerLlmClient askChatAnswerLlmClient;
private final AskChatAnswerProperties askChatAnswerProperties;
private final AskChatTurnReservationService askChatTurnReservationService;
private final AskChatWalletRepository askChatWalletRepository;

@Transactional
public AskChatQuestionSendResponse sendQuestion(Long userId, Long sessionId, String content) {
Expand All @@ -70,6 +72,7 @@ public AskChatQuestionSendResponse sendQuestion(Long userId, Long sessionId, Str

AskChatMessage assistantMessage;
AskChatAnswerGenerationResponse answerGeneration;
int remainingMessageCount;
List<String> followUpQuestions;
try {
AskChatAnswerGenerationResult generationResult = askChatAnswerLlmClient.generate(context);
Expand All @@ -78,12 +81,14 @@ public AskChatQuestionSendResponse sendQuestion(Long userId, Long sessionId, Str
saveMessageReferences(assistantMessage, generationResult);
askChatTurnReservationService.confirm(turnReservation);
session = completeAnsweredTurn(userId, session.getId());
remainingMessageCount = getRemainingMessageCount(userId);
answerGeneration = AskChatAnswerGenerationResponse.completed();
followUpQuestions = generationResult.answer().followUpQuestions();
} catch (AiServiceException e) {
long generationDurationMs = elapsedMillis(generationStartedAt);
saveFailedAssistantMessage(session, e, generationDurationMs);
askChatTurnReservationService.refund(userId, session, turnReservation);
remainingMessageCount = getRemainingMessageCount(userId);
assistantMessage = null;
answerGeneration = AskChatAnswerGenerationResponse.failed(
e.getErrorCode(),
Expand All @@ -97,6 +102,7 @@ public AskChatQuestionSendResponse sendQuestion(Long userId, Long sessionId, Str
AskChatMessageResponse.from(userMessage),
assistantMessage == null ? null : AskChatMessageResponse.from(assistantMessage),
answerGeneration,
remainingMessageCount,
followUpQuestions
);
}
Expand Down Expand Up @@ -159,6 +165,12 @@ private AskChatSession getSession(Long userId, Long sessionId) {
.orElseThrow(() -> new NotFoundException(ErrorCode.ASK_CHAT_SESSION_NOT_FOUND));
}

private int getRemainingMessageCount(Long userId) {
return askChatWalletRepository.findByUserId(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.ASK_CHAT_WALLET_NOT_FOUND))
.getTotalTurnBalance();
}

private AskChatSession completeAnsweredTurn(Long userId, Long sessionId) {
int updated = askChatSessionRepository.completeAnsweredTurn(
sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatHomeResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatQuestionSendResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatRemainingMessageCountResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatSampleQuestionResponse;
import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatSampleQuestion;
import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatSession;
Expand Down Expand Up @@ -70,6 +71,14 @@ public AskChatQuestionSendResponse startSession(Long userId, String content) {
return askChatMessageCommandService.sendQuestion(userId, session.getId(), content);
}

@Transactional(readOnly = true)
public AskChatRemainingMessageCountResponse getRemainingMessageCount(Long userId) {
AskChatWallet askChatWallet = askChatWalletRepository.findByUserId(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.ASK_CHAT_WALLET_NOT_FOUND));

return new AskChatRemainingMessageCountResponse(askChatWallet.getTotalTurnBalance());
}

private void validateMinimumAnswerCount(Long userId) {
if (answerEntryRepository.countByUserId(userId) < MIN_ANSWER_COUNT_TO_USE_ASK_CHAT) {
throw new BadRequestException(ErrorCode.ASK_CHAT_NOT_ENOUGH_ANSWERS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatHomeResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatMessageResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatQuestionSendResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatRemainingMessageCountResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatSampleQuestionResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatSessionResponse;
import com.devkor.ifive.nadab.domain.askchat.api.dto.response.AskChatTurnChargeResponse;
Expand Down Expand Up @@ -98,6 +99,7 @@ void startSession_delegates_first_question_to_session_service() {
ErrorCode.AI_RESPONSE_PARSE_FAILED,
"generation failed"
),
9,
List.of()
);
when(askChatSessionService.startSession(1L, "나는 어떤 사람이야?")).thenReturn(sendResponse);
Expand Down Expand Up @@ -138,6 +140,7 @@ void sendQuestion_delegates_user_session_and_content_to_message_service() {
ErrorCode.AI_RESPONSE_PARSE_FAILED,
"generation failed"
),
9,
List.of()
);
when(askChatMessageCommandService.sendQuestion(1L, 10L, "question")).thenReturn(sendResponse);
Expand Down Expand Up @@ -174,6 +177,23 @@ void chargeTurns_delegates_to_wallet_charge_service() {
verify(askChatWalletChargeService).chargeTurns(1L);
}

@Test
void getRemainingTurns_returns_remaining_message_count_only() {
AskChatSessionController controller = controller();
UserPrincipal principal = new UserPrincipal(1L);
AskChatRemainingMessageCountResponse remainingResponse =
new AskChatRemainingMessageCountResponse(9);
when(askChatSessionService.getRemainingMessageCount(1L)).thenReturn(remainingResponse);

ResponseEntity<ApiResponseDto<AskChatRemainingMessageCountResponse>> response =
controller.getRemainingTurns(principal);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getData().remainingMessageCount()).isEqualTo(9);
verify(askChatSessionService).getRemainingMessageCount(1L);
}

private AskChatSessionController controller() {
return new AskChatSessionController(
askChatSessionService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatRagDocument;
import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatSession;
import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatSessionStatus;
import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatWallet;
import com.devkor.ifive.nadab.domain.askchat.core.properties.AskChatAnswerProperties;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatMessageReferenceRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatMessageRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatRagDocumentRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatSessionRepository;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatWalletRepository;
import com.devkor.ifive.nadab.domain.askchat.infra.AskChatAnswerLlmClient;
import com.devkor.ifive.nadab.domain.user.core.entity.User;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.BadRequestException;
import com.devkor.ifive.nadab.global.exception.ConflictException;
Expand Down Expand Up @@ -75,6 +78,9 @@ class AskChatMessageCommandServiceTest {
@Mock
private AskChatTurnReservationService askChatTurnReservationService;

@Mock
private AskChatWalletRepository askChatWalletRepository;

private AskChatMessageCommandService service;
private AskChatAnswerProperties askChatAnswerProperties;

Expand All @@ -91,7 +97,8 @@ void setUp() {
askChatAnswerContextService,
askChatAnswerLlmClient,
askChatAnswerProperties,
askChatTurnReservationService
askChatTurnReservationService,
askChatWalletRepository
);
}

Expand Down Expand Up @@ -127,11 +134,13 @@ void sendQuestion_saves_user_message_to_requested_session() {
eq(AskChatSessionService.MAX_TURN_COUNT),
any(OffsetDateTime.class)
)).thenReturn(1);
when(askChatWalletRepository.findByUserId(1L)).thenReturn(Optional.of(wallet(3, 5)));

var response = service.sendQuestion(1L, 10L, " 나는 어떤 사람이야? ");

assertThat(response.session().sessionId()).isEqualTo(10L);
assertThat(response.session().answeredTurnCount()).isEqualTo(3);
assertThat(response.remainingMessageCount()).isEqualTo(8);
assertThat(response.userMessage().role()).isEqualTo(AskChatMessageRole.USER);
assertThat(response.userMessage().status()).isEqualTo(AskChatMessageStatus.COMPLETED);
assertThat(response.userMessage().content()).isEqualTo("나는 어떤 사람이야?");
Expand Down Expand Up @@ -205,11 +214,13 @@ void sendQuestion_saves_failed_assistant_message_when_answer_generation_fails()
when(askChatTurnReservationService.reserveTurn(1L, activeSession)).thenReturn(reservation);
when(askChatAnswerLlmClient.generate(context))
.thenThrow(new AiResponseParseException(ErrorCode.AI_RESPONSE_PARSE_FAILED));
when(askChatWalletRepository.findByUserId(1L)).thenReturn(Optional.of(wallet(2, 7)));

var response = service.sendQuestion(1L, 11L, "나는 어떤 사람이야?");

assertThat(response.userMessage().status()).isEqualTo(AskChatMessageStatus.COMPLETED);
assertThat(response.assistantMessage()).isNull();
assertThat(response.remainingMessageCount()).isEqualTo(9);
assertThat(response.answerGeneration().success()).isFalse();
assertThat(response.answerGeneration().errorCode()).isEqualTo(ErrorCode.AI_RESPONSE_PARSE_FAILED.getCode());
assertThat(response.answerGeneration().message()).isEqualTo("답변 생성에 오류가 발생했어요. 다시 시도해주세요.");
Expand Down Expand Up @@ -331,6 +342,10 @@ private AskChatTurnReservation reservation() {
return new AskChatTurnReservation(1000L, -1, 0);
}

private AskChatWallet wallet(int freeTurnBalance, int paidTurnBalance) {
return AskChatWallet.create(mock(User.class), freeTurnBalance, paidTurnBalance);
}

private AskChatSession session(
Long id,
AskChatSessionStatus status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ void startSession_creates_new_session_and_sends_first_question() {
verifyNoInteractions(answerEntryRepository);
}

@Test
void getRemainingMessageCount_returns_total_turn_balance() {
User user = mock(User.class);
when(askChatWalletRepository.findByUserId(1L))
.thenReturn(Optional.of(AskChatWallet.create(user, 2, 7)));

var response = service.getRemainingMessageCount(1L);

assertThat(response.remainingMessageCount()).isEqualTo(9);
verifyNoInteractions(answerEntryRepository);
verifyNoInteractions(userRepository);
verifyNoInteractions(userWalletRepository);
verifyNoInteractions(askChatSampleQuestionRepository);
verify(askChatSessionRepository, never()).save(any());
}

@Test
void startSession_rejects_missing_user_when_creating_session() {
when(userRepository.findById(1L)).thenReturn(Optional.empty());
Expand Down
Loading