Files
a301_mmo_game_server/MMOTestServer/MMOserver/Api/RestApi.cs
tolelom 46dd92b27d feat: 보스레이드 연동 — 입장 요청, 토큰 검증, 결과 보고 API 추가
- RestApi에 보스레이드 입장/검증/시작/완료/실패 엔드포인트 추가
- GameServer에 보스레이드 흐름 처리 로직
- Player 모델에 보스레이드 상태 필드 추가
- 보스레이드 관련 패킷 정의

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 17:51:33 +09:00

135 lines
4.6 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using MMOserver.Utils;
using Serilog;
namespace MMOserver.Api;
public class RestApi : Singleton<RestApi>
{
private const string VERIFY_URL = "https://a301.api.tolelom.xyz";
private const string INTERNAL_API_KEY = "017f15b28143fc67d2e5bed283c37d2da858b9f294990a5334238e055e3f5425";
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", INTERNAL_API_KEY);
}
// 토큰 검증 - 성공 시 username 반환
// 401 → 재시도 없이 즉시 null 반환 (토큰 자체가 무효)
// 타임아웃/네트워크 오류 → 최대 MAX_RETRY회 재시도 후 null 반환
public async Task<string?> VerifyTokenAsync(string token)
{
string url = VERIFY_URL + "/api/internal/auth/verify";
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 null;
}
response.EnsureSuccessStatusCode();
AuthVerifyResponse? result = await response.Content.ReadFromJsonAsync<AuthVerifyResponse>();
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 null;
}
// 플레이어 프로필 조회 - 성공 시 PlayerProfileResponse 반환
public async Task<PlayerProfileResponse?> GetPlayerProfileAsync(string username)
{
string url = VERIFY_URL + "/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<PlayerProfileResponse>();
}
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; }
}
}