using System.Globalization; using System.Text.Json; namespace Infrastructure.StockData; /// /// KOSIS statisticsData.do JSON 응답 파서 — 순수 C# (System.Text.Json). /// 응답 골격: 성공 시 최상위 JSON 배열 [ { PRD_DE, DT, UNIT_NM, ITM_NM, C1, C1_NM, TBL_ID, ... }, ... ]. /// 오류 시엔 배열이 아닌 객체 { "err":"21", "errMsg":"..." } → 예외 없이 빈 목록 반환 (koreaexim 파서와 동일한 방어). /// - PRD_DE(수록시점) → Period, DT(수치) → Value, UNIT_NM(단위) → Unit, ITM_NM/C1/C1_NM → 계열 식별용 보조. /// - KOSIS quirk: 통계표 하나에 여러 분류(C1 시도별/지수별)가 섞여 오므로, 계열 특정은 상위(수집 배치)가 C1 필터로 수행한다. /// - DT 는 문자열로 오며 미고시 시점은 ""·"-" 로 온다 → Value 파싱 실패 행은 skip (해당 시점 미적재, 다음 실행 재시도). /// public static class KosisParser { /// KOSIS 데이터 1행. Value 파싱 실패(빈값/"-") 행은 목록에서 제외되므로 여기 Value 는 항상 유효. public sealed record Row( string Period, decimal Value, string? Unit, string ItemName, string ClassCode, string ClassName ); public static IReadOnlyList Parse(string json) { var rows = new List(); 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; } var period = GetString(item, "PRD_DE"); if (period.Length == 0) { continue; } var value = ParseNullableDecimal(GetString(item, "DT")); if (value is null) { continue; } var unit = GetString(item, "UNIT_NM"); rows.Add(new Row( period, value.Value, unit.Length == 0 ? null : unit, GetString(item, "ITM_NM"), GetString(item, "C1"), GetString(item, "C1_NM"))); } return rows; } 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 }; } /// 수치 문자열 → decimal (천단위 콤마 제거). ""·"-"·"X" 등 파싱 실패는 null. private static decimal? ParseNullableDecimal(string raw) { return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null; } }