| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- using System.Globalization;
- using System.Text.Json;
- namespace Infrastructure.StockData;
- /// <summary>
- /// 한국수출입은행 OpenAPI AP02(대출금리) / AP03(국제금리) 응답 파서 — 순수 C# (System.Text.Json).
- /// 응답 골격: 최상위가 JSON 배열 [ { result, cur_nm(항목명), … 금리 수치 필드 }, ... ]. (AP01 환율과 같은 배열 shape, 필드만 다름)
- /// - result: 각 행의 조회 결과 코드 (1=성공). result != 1 인 행은 skip (2~4 = DATA/인증/제한 오류).
- /// - ItemName ← cur_nm (또는 대응 항목명 필드). 값은 전부 문자열, 천단위 콤마/빈값 가능.
- /// - Rate ← 항목의 금리 수치. koreaexim 금리 응답은 종류별로 수치 필드명이 달라, 알려진 후보 필드를 순서대로 탐색해
- /// 처음 파싱되는 숫자를 대표 금리로 삼는다 (deal_bas_r, ttb, tts, bkpr 등). 아무 것도 없으면 Rate=null (항목만 보존).
- /// koreaexim quirk: 주말/공휴일·오전 이전엔 빈 배열([]), 키/파라미터 오류 시 result 2~4 또는 빈 응답 → 예외 없이 빈 목록 반환.
- /// </summary>
- public static class KoreaEximRateParser
- {
- /// <summary>금리 1행 (searchdate 는 호출부가 채움). Rate 는 미고시 시 null.</summary>
- public sealed record RateRow(
- string ItemName,
- decimal? Rate
- );
- // 금리 수치가 담길 수 있는 후보 필드 (순서대로 첫 파싱 성공값을 채택)
- private static readonly string[] RateFields = ["deal_bas_r", "ttb", "tts", "bkpr", "kftc_deal_bas_r"];
- // 항목명이 담길 수 있는 후보 필드
- private static readonly string[] NameFields = ["cur_nm", "cur_unit", "item_nm"];
- public static IReadOnlyList<RateRow> Parse(string json)
- {
- var rows = new List<RateRow>();
- if (string.IsNullOrWhiteSpace(json))
- {
- return rows;
- }
- using var doc = JsonDocument.Parse(json);
- if (doc.RootElement.ValueKind != JsonValueKind.Array)
- {
- return rows;
- }
- foreach (var item in doc.RootElement.EnumerateArray())
- {
- if (item.ValueKind != JsonValueKind.Object)
- {
- continue;
- }
- if (GetInt(item, "result") != 1)
- {
- continue;
- }
- var itemName = FirstNonEmpty(item, NameFields);
- if (itemName.Length == 0)
- {
- continue;
- }
- rows.Add(new RateRow(itemName, FirstRate(item, RateFields)));
- }
- return rows;
- }
- private static string FirstNonEmpty(JsonElement element, string[] fields)
- {
- foreach (var field in fields)
- {
- var value = GetString(element, field);
- if (value.Length > 0)
- {
- return value;
- }
- }
- return string.Empty;
- }
- private static decimal? FirstRate(JsonElement element, string[] fields)
- {
- foreach (var field in fields)
- {
- var value = ParseNullableDecimal(GetString(element, field));
- if (value is not null)
- {
- return value;
- }
- }
- return null;
- }
- private static string GetString(JsonElement element, string name)
- {
- if (!element.TryGetProperty(name, out var value))
- {
- return string.Empty;
- }
- return value.ValueKind switch
- {
- JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
- JsonValueKind.Number => value.GetRawText(),
- _ => string.Empty
- };
- }
- private static int GetInt(JsonElement element, string name)
- {
- if (!element.TryGetProperty(name, out var value))
- {
- return 0;
- }
- return value.ValueKind switch
- {
- JsonValueKind.Number => value.TryGetInt32(out var n) ? n : 0,
- JsonValueKind.String => int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var s) ? s : 0,
- _ => 0
- };
- }
- /// <summary>금리 문자열 → decimal (천단위 콤마 제거). "" 나 "-" 등 파싱 실패는 null.</summary>
- private static decimal? ParseNullableDecimal(string raw)
- {
- return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
- }
- }
|