Skip to content

Commit c280132

Browse files
committed
feat: 토큰 지급과 비용 추적
1 parent e29a9fb commit c280132

File tree

9 files changed

+364
-221
lines changed

9 files changed

+364
-221
lines changed

ProjectVG.Api/Controllers/ConversationController.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@ public async Task<IActionResult> GetConversationHistory(Guid characterId, [FromQ
3535
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
3636
if (string.IsNullOrEmpty(userId) || !Guid.TryParse(userId, out var userGuid))
3737
{
38+
_logger.LogWarning("Authentication failed: Invalid user ID in JWT token");
3839
throw new ValidationException(ErrorCode.AUTHENTICATION_FAILED);
3940
}
4041

42+
_logger.LogDebug("Fetching conversation history: UserId={UserId}, CharacterId={CharacterId}, Page={Page}, PageSize={PageSize}",
43+
userGuid, characterId, request.Page, request.PageSize);
44+
4145
// 대화 기록 조회
4246
var messages = await _conversationService.GetConversationHistoryAsync(userGuid, characterId, request.Page, request.PageSize);
4347
var totalCount = await _conversationService.GetMessageCountAsync(userGuid, characterId);
48+
49+
_logger.LogInformation("Conversation history retrieved: UserId={UserId}, CharacterId={CharacterId}, MessageCount={MessageCount}, TotalCount={TotalCount}",
50+
userGuid, characterId, messages.Count(), totalCount);
4451

4552
// 응답 매핑
4653
var response = new ConversationHistoryListResponse

ProjectVG.Application/Models/Chat/ChatProcessResultMessage.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ public record ChatProcessResultMessage
3434
[JsonPropertyName("order")]
3535
public int Order { get; init; }
3636

37+
[JsonPropertyName("tokens_used")]
38+
public decimal? TokensUsed { get; init; }
39+
40+
[JsonPropertyName("tokens_remaining")]
41+
public decimal? TokensRemaining { get; init; }
42+
3743
public static ChatProcessResultMessage FromSegment(ChatSegment segment, string? requestId = null)
3844
{
3945
var audioData = segment.HasAudio ? Convert.ToBase64String(segment.AudioData!) : null;
@@ -65,5 +71,10 @@ public ChatProcessResultMessage WithAudioData(byte[]? audioBytes)
6571
return this with { AudioData = null };
6672
}
6773
}
74+
75+
public ChatProcessResultMessage WithTokenInfo(decimal? tokensUsed, decimal? tokensRemaining)
76+
{
77+
return this with { TokensUsed = tokensUsed, TokensRemaining = tokensRemaining };
78+
}
6879
}
6980
}

ProjectVG.Application/Services/Auth/AuthService.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,15 @@ public async Task<AuthResult> LoginWithOAuthAsync(string provider, string provid
9797
}
9898

9999
// 첫 로그인 토큰 지급 시도
100-
await _tokenManagementService.GrantInitialTokensAsync(user.Id);
100+
var tokenGranted = await _tokenManagementService.GrantInitialTokensAsync(user.Id);
101+
if (tokenGranted)
102+
{
103+
_logger.LogInformation("Initial tokens (5000) granted successfully to user {UserId}", user.Id);
104+
}
105+
else
106+
{
107+
_logger.LogInformation("Initial tokens already granted or grant failed for user {UserId}", user.Id);
108+
}
101109

102110
var tokens = await _tokenService.GenerateTokensAsync(user.Id);
103111

ProjectVG.Application/Services/Chat/ChatService.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public class ChatService : IChatService
2929
private readonly ICostTrackingDecorator<ChatTTSProcessor> _ttsProcessor;
3030
private readonly ChatResultProcessor _resultProcessor;
3131

32-
private readonly ChatSuccessHandler _chatSuccessHandler;
3332
private readonly ChatFailureHandler _chatFailureHandler;
3433

3534
public ChatService(
@@ -46,7 +45,6 @@ public ChatService(
4645
ICostTrackingDecorator<ChatTTSProcessor> ttsProcessor,
4746
ChatResultProcessor resultProcessor,
4847

49-
ChatSuccessHandler chatSuccessHandler,
5048
ChatFailureHandler chatFailureHandler
5149
) {
5250
_metricsService = metricsService;
@@ -62,7 +60,6 @@ ChatFailureHandler chatFailureHandler
6260
_llmProcessor = llmProcessor;
6361
_ttsProcessor = ttsProcessor;
6462
_resultProcessor = resultProcessor;
65-
_chatSuccessHandler = chatSuccessHandler;
6663
_chatFailureHandler = chatFailureHandler;
6764
}
6865

@@ -115,10 +112,12 @@ private async Task ProcessChatRequestInternalAsync(ChatProcessContext context)
115112
await _llmProcessor.ProcessAsync(context);
116113
await _ttsProcessor.ProcessAsync(context);
117114

118-
await _chatSuccessHandler.HandleAsync(context);
119-
115+
// ChatSuccessHandler와 ChatResultProcessor를 같은 스코프에서 실행
120116
using var scope = _scopeFactory.CreateScope();
117+
var successHandler = scope.ServiceProvider.GetRequiredService<ChatSuccessHandler>();
121118
var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>();
119+
120+
await successHandler.HandleAsync(context);
122121
await resultProcessor.PersistResultsAsync(context);
123122
}
124123
catch (Exception) {

ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,26 @@ public async Task HandleAsync(ChatProcessContext context)
4040
var requestId = context.RequestId.ToString();
4141
var userId = context.UserId.ToString();
4242

43+
// 토큰 차감 및 잔액 정보 수집
44+
decimal? tokensUsed = null;
45+
decimal? tokensRemaining = null;
46+
47+
if (context.Cost > 0)
48+
{
49+
var tokenDeductionResult = await DeductTokensForChatAsync(context);
50+
if (tokenDeductionResult.Success)
51+
{
52+
tokensUsed = (decimal)context.Cost;
53+
tokensRemaining = tokenDeductionResult.BalanceAfter;
54+
}
55+
}
56+
4357
foreach (var segment in validSegments)
4458
{
4559
try
4660
{
47-
var message = ChatProcessResultMessage.FromSegment(segment, requestId);
61+
var message = ChatProcessResultMessage.FromSegment(segment, requestId)
62+
.WithTokenInfo(tokensUsed, tokensRemaining);
4863
var wsMessage = new WebSocketMessage("chat", message);
4964

5065
await _webSocketService.SendAsync(userId, wsMessage);
@@ -57,11 +72,8 @@ public async Task HandleAsync(ChatProcessContext context)
5772
}
5873
}
5974

60-
_logger.LogDebug("채팅 결과 전송 완료: 요청 {RequestId}, 세그먼트 {SegmentCount}개",
61-
context.RequestId, validSegments.Count);
62-
63-
// 성공적인 전송 후 토큰 차감 처리
64-
await DeductTokensForSuccessfulChatAsync(context);
75+
_logger.LogDebug("채팅 결과 전송 완료: 요청 {RequestId}, 세그먼트 {SegmentCount}개, 토큰 사용: {TokensUsed}, 잔액: {TokensRemaining}",
76+
context.RequestId, validSegments.Count, tokensUsed, tokensRemaining);
6577
}
6678
catch (Exception ex)
6779
{
@@ -71,48 +83,40 @@ public async Task HandleAsync(ChatProcessContext context)
7183
}
7284

7385
/// <summary>
74-
/// 성공적인 채팅 처리 후 토큰 차감
86+
/// 채팅 처리를 위한 토큰 차감
7587
/// </summary>
76-
private async Task DeductTokensForSuccessfulChatAsync(ChatProcessContext context)
88+
private async Task<TokenTransactionResult> DeductTokensForChatAsync(ChatProcessContext context)
7789
{
7890
try
7991
{
80-
// 실제 사용된 Cost를 토큰으로 차감
81-
if (context.Cost > 0)
82-
{
83-
var transactionId = $"CHAT_{context.RequestId}_{DateTime.UtcNow:yyyyMMddHHmmss}";
84-
var result = await _tokenManagementService.DeductTokensAsync(
85-
context.UserId,
86-
(decimal)context.Cost,
87-
transactionId,
88-
"CHAT_USAGE",
89-
$"채팅 사용료 - 캐릭터: {context.CharacterId}",
90-
context.RequestId.ToString(),
91-
"ChatSession"
92-
);
92+
var transactionId = $"CHAT_{context.RequestId}_{DateTime.UtcNow:yyyyMMddHHmmss}";
93+
var result = await _tokenManagementService.DeductTokensAsync(
94+
context.UserId,
95+
(decimal)context.Cost,
96+
transactionId,
97+
"CHAT_USAGE",
98+
$"채팅 사용료 - 캐릭터: {context.CharacterId}",
99+
context.RequestId.ToString(),
100+
"ChatSession"
101+
);
93102

94-
if (result.Success)
95-
{
96-
_logger.LogInformation("채팅 토큰 차감 완료: {UserId}, 차감 토큰: {Cost}, 잔액: {Balance}",
97-
context.UserId, context.Cost, result.BalanceAfter);
98-
}
99-
else
100-
{
101-
_logger.LogError("채팅 토큰 차감 실패: {UserId}, 에러: {Error}",
102-
context.UserId, result.ErrorMessage);
103-
// 토큰 차감 실패는 로그만 남기고 사용자에게는 이미 성공 응답을 보냈으므로 예외를 던지지 않음
104-
}
103+
if (result.Success)
104+
{
105+
_logger.LogInformation("채팅 토큰 차감 완료: {UserId}, 차감 토큰: {Cost}, 잔액: {Balance}",
106+
context.UserId, context.Cost, result.BalanceAfter);
105107
}
106108
else
107109
{
108-
_logger.LogWarning("채팅 처리 완료했지만 Cost가 0 또는 음수: {RequestId}, Cost: {Cost}",
109-
context.RequestId, context.Cost);
110+
_logger.LogError("채팅 토큰 차감 실패: {UserId}, 에러: {Error}",
111+
context.UserId, result.ErrorMessage);
110112
}
113+
114+
return result;
111115
}
112116
catch (Exception ex)
113117
{
114118
_logger.LogError(ex, "채팅 토큰 차감 처리 중 예외 발생: {RequestId}", context.RequestId);
115-
// 토큰 차감 실패는 사용자 경험에 영향을 주지 않도록 예외를 삼킴
119+
return TokenTransactionResult.CreateFailure($"토큰 차감 처리 중 예외 발생: {ex.Message}");
116120
}
117121
}
118122
}

ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,19 @@ public async Task ValidateAsync(ChatRequestCommand command)
4949
}
5050

5151
// 토큰 잔액 검증 - 예상 비용으로 미리 확인
52-
var hasSufficientTokens = await _tokenManagementService.HasSufficientTokensAsync(command.UserId, ESTIMATED_CHAT_COST);
52+
var balance = await _tokenManagementService.GetTokenBalanceAsync(command.UserId);
53+
var currentBalance = balance.CurrentBalance;
54+
55+
if (currentBalance <= 0) {
56+
_logger.LogWarning("토큰 잔액 부족 (0 토큰): UserId={UserId}", command.UserId);
57+
throw new ValidationException(ErrorCode.INSUFFICIENT_TOKEN_BALANCE, $"토큰이 부족합니다. 현재 잔액: {currentBalance} 토큰, 필요 토큰: {ESTIMATED_CHAT_COST} 토큰");
58+
}
59+
60+
var hasSufficientTokens = currentBalance >= ESTIMATED_CHAT_COST;
5361
if (!hasSufficientTokens) {
54-
_logger.LogWarning("토큰 부족: {UserId}, 필요 토큰: {RequiredTokens}", command.UserId, ESTIMATED_CHAT_COST);
55-
var balance = await _tokenManagementService.GetTokenBalanceAsync(command.UserId);
56-
throw new ValidationException(ErrorCode.INSUFFICIENT_TOKEN_BALANCE);
62+
_logger.LogWarning("토큰 부족: UserId={UserId}, 현재잔액={CurrentBalance}, 필요토큰={RequiredTokens}",
63+
command.UserId, currentBalance, ESTIMATED_CHAT_COST);
64+
throw new ValidationException(ErrorCode.INSUFFICIENT_TOKEN_BALANCE, $"토큰이 부족합니다. 현재 잔액: {currentBalance} 토큰, 필요 토큰: {ESTIMATED_CHAT_COST} 토큰");
5765
}
5866

5967
_logger.LogDebug("채팅 요청 검증 완료: {UserId}, {CharacterId}", command.UserId, command.CharacterId);

0 commit comments

Comments
 (0)