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
@@ -1,13 +1,16 @@
package com.devkor.ifive.nadab.domain.admin.api;

import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAskChatRagBackfillResponse;
import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAskChatRagBackfillStatusResponse;
import com.devkor.ifive.nadab.domain.askchat.application.AskChatRagBackfillService;
import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillResultDto;
import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto;
import com.devkor.ifive.nadab.global.core.response.ApiResponseDto;
import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -20,6 +23,12 @@ public class AdminAskChatRagController {

private final AskChatRagBackfillService askChatRagBackfillService;

@GetMapping("/backfill/daily-answers/status")
public ResponseEntity<ApiResponseDto<AdminAskChatRagBackfillStatusResponse>> getDailyAnswerBackfillStatus() {
AskChatRagBackfillStatusDto status = askChatRagBackfillService.getCompletedDailyAnswerStatus();
return ApiResponseEntity.ok(AdminAskChatRagBackfillStatusResponse.from(status));
}

@PostMapping("/backfill/daily-answers")
public ResponseEntity<ApiResponseDto<AdminAskChatRagBackfillResponse>> backfillDailyAnswers() {
AskChatRagBackfillResultDto result = askChatRagBackfillService.backfillCompletedDailyAnswers();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto;

public record AdminAskChatRagBackfillStatusResponse(
long targetCount,
long indexedCount,
long failedCount
) {

public static AdminAskChatRagBackfillStatusResponse from(AskChatRagBackfillStatusDto status) {
return new AdminAskChatRagBackfillStatusResponse(
status.targetCount(),
status.indexedCount(),
status.failedCount()
);
}
}
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
@@ -1,6 +1,7 @@
package com.devkor.ifive.nadab.domain.askchat.application;

import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillResultDto;
import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto;
import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillTargetDto;
import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatRagBackfillQueryRepository;
import com.devkor.ifive.nadab.domain.askchat.infra.AskChatEmbeddingClient;
Expand All @@ -19,6 +20,10 @@ public class AskChatRagBackfillService {
private final AskChatRagIndexingService indexingService;
private final AskChatEmbeddingClient embeddingClient;

public AskChatRagBackfillStatusDto getCompletedDailyAnswerStatus() {
return backfillQueryRepository.findCompletedDailyAnswerStatus(embeddingClient.version());
}

public AskChatRagBackfillResultDto backfillCompletedDailyAnswers() {
List<AskChatRagBackfillTargetDto> targets = backfillQueryRepository.findCompletedDailyAnswerTargets(
embeddingClient.version(),
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
@@ -0,0 +1,8 @@
package com.devkor.ifive.nadab.domain.askchat.core.dto;

public record AskChatRagBackfillStatusDto(
long targetCount,
long indexedCount,
long failedCount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public class AskChatAnswerProperties {
private LlmProvider provider = LlmProvider.OPENAI;

@NotBlank
private String model = "gpt-4o-mini";
private String model = "gpt-5.6-luna";

@DecimalMin("0.0")
@DecimalMax("2.0")
private double temperature = 0.3;
private double temperature = 1.0;

@Min(1)
private int maxTokens = 900;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.devkor.ifive.nadab.domain.askchat.core.repository;

import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto;
import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillTargetDto;
import com.devkor.ifive.nadab.domain.user.core.entity.InterestCode;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -43,6 +44,33 @@ AND NOT EXISTS (
.addValue("limit", limit), this::mapTarget);
}

public AskChatRagBackfillStatusDto findCompletedDailyAnswerStatus(int embeddingVersion) {
String sql = """
SELECT COUNT(*) FILTER (WHERE d.id IS NULL) AS target_count,
COUNT(*) FILTER (WHERE d.embedding_status = 'COMPLETED') AS indexed_count,
COUNT(*) FILTER (
WHERE d.embedding_status IN ('FAILED', 'DEAD_LETTER')
) AS failed_count
FROM daily_reports dr
JOIN answer_entries ae ON ae.id = dr.answer_entry_id
LEFT JOIN ask_chat_rag_documents d
ON d.source_type = 'ANSWER_ENTRY'
AND d.source_id = ae.id
AND d.embedding_version = :embeddingVersion
WHERE dr.status = 'COMPLETED'
""";

return jdbcTemplate.queryForObject(
sql,
new MapSqlParameterSource("embeddingVersion", embeddingVersion),
(rs, rowNum) -> new AskChatRagBackfillStatusDto(
rs.getLong("target_count"),
rs.getLong("indexed_count"),
rs.getLong("failed_count")
)
);
}

private AskChatRagBackfillTargetDto mapTarget(ResultSet rs, int rowNum) throws SQLException {
String interestCode = rs.getString("interest_code");
return new AskChatRagBackfillTargetDto(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private OpenAiChatOptions options() {
return OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.maxCompletionTokens(properties.getMaxTokens())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class LocalAuthController {
@Operation(
summary = "더미 유저 로그인",
description = """
로컬 환경에서 더미 유저로 로그인하여 액세스 토큰을 발급받습니다.
로컬 환경에서 더미 유저로 로그인하여 액세스 토큰을 발급받습니다. <br/>
더미 유저가 없을 시에는 자동 생성합니다.
"""
)
public ResponseEntity<ApiResponseDto<LocalTokenResponse>> dummyLogin() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,38 @@
package com.devkor.ifive.nadab.domain.auth.application;

import com.devkor.ifive.nadab.domain.auth.api.dto.response.LocalTokenResponse;
import com.devkor.ifive.nadab.domain.auth.infra.LocalDummyUserRepository;
import com.devkor.ifive.nadab.domain.user.core.entity.User;
import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository;
import com.devkor.ifive.nadab.domain.wallet.core.entity.UserWallet;
import com.devkor.ifive.nadab.domain.wallet.core.repository.UserWalletRepository;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import com.devkor.ifive.nadab.global.security.token.AccessTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Profile("local")
@Service
@Transactional
@RequiredArgsConstructor
public class LocalTokenService {

private final AccessTokenProvider accessTokenProvider;
private final UserRepository userRepository;
private final UserWalletRepository userWalletRepository;
private final LocalDummyUserRepository localDummyUserRepository;

// 로컬 로그인용 토큰 발급
public LocalTokenResponse issueDummyAccessToken() {
User user = userRepository.findById(11111L)
Long dummyUserId = localDummyUserRepository.createIfAbsent();
User user = userRepository.findById(dummyUserId)
.orElseThrow(() -> new NotFoundException(ErrorCode.AUTH_DUMMY_USER_NOT_FOUND));

UserWallet wallet = getOrCreateWallet(user);

List<String> roles = List.of("USER");
String accessToken = accessTokenProvider.generateToken(user.getId(), roles);

return new LocalTokenResponse(accessToken);
}

public UserWallet getOrCreateWallet(User user) {
return userWalletRepository.findByUserId(user.getId())
.orElseGet(() -> userWalletRepository.save(UserWallet.create(user)));
}

}
Loading
Loading