using Application.Abstractions.Cache; using Application.Abstractions.Chat; using Application.Abstractions.Data; using Application.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Infrastructure.Chat; /// /// 채팅 보상 훅 실구현 (d0 §2.2 / d3 M2). SendMessage 성공 시 ChatExpConfig.ChatXpPerMessage 를 /// RewardService 로 지급(ActionType=Chat, 일일 캡 적용, XP 전용)한다. /// Singleton 등록(허브 계약 유지)이므로 scoped DbContext/Cache 는 스코프를 직접 생성해 사용한다. /// 예외는 밖으로 던지지 않는다 — 채팅 브로드캐스트에 영향 금지. /// internal sealed class RewardChatHook(IServiceScopeFactory scopeFactory, ILogger logger) : IChatRewardHook { public async Task OnMessageSentAsync(int memberID, string roomKey, string content, CancellationToken ct = default) { if (memberID <= 0) { return; } try { await using var scope = scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var cache = scope.ServiceProvider.GetRequiredService(); // ChatExpConfig 는 Config.Get.Response 에 노출되지 않으므로 수치만 DB 에서 직접 조회. var chatExp = await db.Config.AsNoTracking().OrderByDescending(c => c.ID).Select(c => c.ChatExp.ChatXpPerMessage).FirstOrDefaultAsync(ct); if (chatExp <= 0) { return; } await RewardService.GrantForChatAsync(db, cache, memberID, chatExp, DateTime.UtcNow, ct); await db.SaveChangesAsync(ct); } catch (Exception ex) { // 채팅 흐름을 막지 않는다 — 지급 실패만 로깅. logger.LogWarning(ex, "채팅 보상 지급 실패 (memberID={MemberID}, roomKey={RoomKey})", memberID, roomKey); } } }