using System.Net; using System.Net.Http.Headers; namespace Kbx.Shared.Providers; public sealed class KrxOpenApiAdapter(HttpClient http, IKbxProviderSecretStore secrets) : IKbxExternalProviderAdapter { public string ProviderId => "provider.krx.openapi"; public async Task> InvokeApprovedServiceAsync(KrxApprovedServiceRequest request, CancellationToken ct) { if (!request.ServiceUri.IsAbsoluteUri || request.ServiceUri.Scheme != Uri.UriSchemeHttps || !request.ServiceUri.Host.EndsWith(".krx.co.kr", StringComparison.OrdinalIgnoreCase)) return new(KbxProviderResultKind.PermanentFailure, Code:"KRX_SERVICE_URL_NOT_APPROVED", Message:"KRX 승인 서비스의 HTTPS URL만 사용할 수 있습니다."); if (request.Method != HttpMethod.Get && request.Method != HttpMethod.Post) return new(KbxProviderResultKind.PermanentFailure, Code:"KRX_METHOD_NOT_ALLOWED", Message:"조회성 GET/POST만 허용됩니다."); var key = await secrets.GetRequiredAsync("ExternalProviders:Krx:AuthKey", ct); var uri = AppendQuery(request.ServiceUri, request.Query); using var message = new HttpRequestMessage(request.Method, uri); message.Headers.TryAddWithoutValidation("AUTH_KEY", key); if (request.Method == HttpMethod.Post && request.Body is not null) message.Content = new StringContent(request.Body, System.Text.Encoding.UTF8, "application/json"); using var response = await http.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, ct); var body = await response.Content.ReadAsStringAsync(ct); if (response.IsSuccessStatusCode) return new(KbxProviderResultKind.Success, body, HttpStatus:(int)response.StatusCode); var transient = response.StatusCode is HttpStatusCode.RequestTimeout or (HttpStatusCode)429 || (int)response.StatusCode >= 500; return new(transient ? KbxProviderResultKind.TransientFailure : KbxProviderResultKind.PermanentFailure, Code:$"HTTP_{(int)response.StatusCode}", Message:"KRX OPEN API 호출이 실패했습니다.", HttpStatus:(int)response.StatusCode); } private static Uri AppendQuery(Uri uri, IReadOnlyDictionary query) { if (query.Count == 0) return uri; var parts = query.Where(x => x.Value is not null).Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value!)}"); var builder = new UriBuilder(uri) { Query = string.Join("&", parts) }; return builder.Uri; } }