using System.Net; using System.Net.Http.Json; using System.Text.Json.Serialization; using MMOserver.Config; using MMOserver.Utils; using Serilog; namespace MMOserver.Api; public class RestApi : Singleton { private readonly HttpClient httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; private const int MAX_RETRY = 3; private static readonly TimeSpan RETRY_DELAY = TimeSpan.FromSeconds(1); public RestApi() { httpClient.DefaultRequestHeaders.Add("X-API-Key", AppConfig.RestApi.ApiKey); } // 토큰 검증 - 성공 시 username 반환 // 401 → 재시도 없이 즉시 null 반환 (토큰 자체가 무효) // 타임아웃/네트워크 오류 → 최대 MAX_RETRY회 재시도 후 null 반환 public async Task VerifyTokenAsync(string token) { string url = AppConfig.RestApi.BaseUrl + AppConfig.RestApi.VerifyToken; for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { try { HttpResponseMessage response = await httpClient.PostAsJsonAsync(url, new { token }); // 401: 토큰 자체가 무효 → 재시도해도 같은 결과, 즉시 반환 if (response.StatusCode == HttpStatusCode.Unauthorized) { Log.Warning("[RestApi] 토큰 인증 실패 (401)"); return ""; } response.EnsureSuccessStatusCode(); AuthVerifyResponse? result = await response.Content.ReadFromJsonAsync(); return result?.Username; } catch (Exception ex) when (attempt < MAX_RETRY) { // 일시적 장애 (타임아웃, 네트워크 오류 등) → 재시도 Log.Warning("[RestApi] 통신 실패 (시도 {Attempt}/{Max}): {Message}", attempt, MAX_RETRY, ex.Message); await Task.Delay(RETRY_DELAY); } catch (Exception ex) { // 마지막 시도까지 실패 Log.Error("[RestApi] 최종 통신 실패 ({Max}회 시도): {Message}", MAX_RETRY, ex.Message); } } return ""; } // 플레이어 프로필 조회 - 성공 시 PlayerProfileResponse 반환 public async Task GetPlayerProfileAsync(string username) { string url = AppConfig.RestApi.BaseUrl + "/api/internal/player/profile?username=" + Uri.EscapeDataString(username); for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { try { HttpResponseMessage response = await httpClient.GetAsync(url); if (response.StatusCode == HttpStatusCode.NotFound) { Log.Warning("[RestApi] 프로필 없음 username={Username}", username); return null; } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } catch (Exception ex) when (attempt < MAX_RETRY) { Log.Warning("[RestApi] 프로필 조회 실패 (시도 {Attempt}/{Max}): {Message}", attempt, MAX_RETRY, ex.Message); await Task.Delay(RETRY_DELAY); } catch (Exception ex) { Log.Error("[RestApi] 프로필 조회 최종 실패 ({Max}회 시도): {Message}", MAX_RETRY, ex.Message); } } return null; } private sealed class AuthVerifyResponse { [JsonPropertyName("username")] public string? Username { get; set; } } public sealed class PlayerProfileResponse { [JsonPropertyName("nickname")] public string Nickname { get; set; } = string.Empty; [JsonPropertyName("level")] public int Level { get; set; } [JsonPropertyName("experience")] public int Experience { get; set; } [JsonPropertyName("nextExp")] public int NextExp { get; set; } [JsonPropertyName("maxHp")] public double MaxHp { get; set; } [JsonPropertyName("maxMp")] public double MaxMp { get; set; } [JsonPropertyName("attackPower")] public double AttackPower { get; set; } [JsonPropertyName("attackRange")] public double AttackRange { get; set; } [JsonPropertyName("sprintMultiplier")] public double SprintMultiplier { get; set; } } // 레이드 채널 접속 여부 체크 // 성공 시 BossRaidResult 반환, 실패/거절 시 null 반환 public async Task BossRaidAccesssAsync(List userNames, int bossId) { string url = AppConfig.RestApi.BaseUrl + AppConfig.RestApi.BossRaidAccess; for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { try { HttpResponseMessage response = await httpClient.PostAsJsonAsync(url, new { usernames = userNames, bossId = bossId }); // 401: API 키 인증 실패 if (response.StatusCode == HttpStatusCode.Unauthorized) { Log.Warning("[RestApi] 보스 레이드 접속 인증 실패 (401)"); return null; } // 400: 입장 조건 미충족 (레벨 부족, 이미 진행중 등) if (response.StatusCode == HttpStatusCode.BadRequest) { Log.Warning("[RestApi] 보스 레이드 입장 거절 (400) BossId={BossId}", bossId); return null; } response.EnsureSuccessStatusCode(); BossRaidAccessResponse? raw = await response.Content.ReadFromJsonAsync(); if (raw == null) { return null; } // API 응답 → 도메인 모델 매핑 return new BossRaidResult { RoomId = raw.RoomId, SessionName = raw.SessionName ?? string.Empty, BossId = raw.BossId, Players = raw.Players, Status = raw.Status ?? string.Empty, Tokens = raw.Tokens }; } catch (Exception ex) when (attempt < MAX_RETRY) { Log.Warning("[RestApi] 보스 레이드 통신 실패 (시도 {Attempt}/{Max}): {Message}", attempt, MAX_RETRY, ex.Message); await Task.Delay(RETRY_DELAY); } catch (Exception ex) { Log.Error("[RestApi] 보스 레이드 최종 통신 실패 ({Max}회 시도): {Message}", MAX_RETRY, ex.Message); } } return null; } private sealed class BossRaidAccessResponse { [JsonPropertyName("roomId")] public int RoomId { get; set; } [JsonPropertyName("sessionName")] public string? SessionName { get; set; } [JsonPropertyName("bossId")] public int BossId { get; set; } [JsonPropertyName("players")] public List Players { get; set; } = new(); [JsonPropertyName("status")] public string? Status { get; set; } [JsonPropertyName("tokens")] public Dictionary? Tokens { get; set; } = new(); } }