using System.Globalization; using System.Text.Json; namespace Infrastructure.StockData; /// /// OpenDART 공시검색(list.json) 응답 파서 — 순수 C# (System.Text.Json). /// 응답 골격: { "status", "message", "page_no", "page_count", "total_count", "total_page", "list":[ {...} ] }. /// status: "000" 정상, "013" 조회된 데이터 없음, "020"/"100" 등은 키/파라미터 오류. list 항목의 모든 값은 문자열. /// - stock_code 는 6자리 단축코드(상장) 또는 "" (비상장) → 빈 값은 null. /// - rcept_dt 는 YYYYMMDD → DateOnly. rm(비고)은 없거나 "" → null. /// list.json 이 stock_code 를 이미 포함하므로 corpCode.xml 매핑 없이 종목별 조회가 가능하다. /// public static class DartDisclosureParser { /// 공시 1행 (list.json 항목). stock_code 빈 값은 null, rcept_dt 는 DateOnly. public sealed record DisclosureRow( string RceptNo, string CorpCode, string CorpName, string? StockCode, string CorpCls, string ReportNm, string FlrNm, DateOnly RceptDt, string? Rm ); /// 파싱 결과 — status/총페이지 등 메타와 파싱된 행. status != "000" 이면 Rows 는 빈 목록. public sealed record ParseResult( string Status, string Message, int PageNo, int TotalPage, int TotalCount, IReadOnlyList Rows ); public static ParseResult Parse(string json) { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; var status = GetString(root, "status"); var message = GetString(root, "message"); var pageNo = GetInt(root, "page_no"); var totalPage = GetInt(root, "total_page"); var totalCount = GetInt(root, "total_count"); var rows = new List(); if (status != "000" || !root.TryGetProperty("list", out var list) || list.ValueKind != JsonValueKind.Array) { return new ParseResult(status, message, pageNo, totalPage, totalCount, rows); } foreach (var item in list.EnumerateArray()) { var rceptNo = GetString(item, "rcept_no"); var corpCode = GetString(item, "corp_code"); var reportNm = GetString(item, "report_nm"); var rceptDt = ParseDate(GetString(item, "rcept_dt")); if (rceptNo.Length == 0 || corpCode.Length == 0 || reportNm.Length == 0 || rceptDt is null) { continue; } rows.Add(new DisclosureRow( rceptNo, corpCode, GetString(item, "corp_name"), ToNull(GetString(item, "stock_code")), GetString(item, "corp_cls"), reportNm, GetString(item, "flr_nm"), rceptDt.Value, ToNull(GetString(item, "rm")) )); } return new ParseResult(status, message, pageNo, totalPage, totalCount, 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 }; } 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 }; } /// 빈 문자열/"-" 은 null, 나머지는 trim 값 private static string? ToNull(string raw) { return raw.Length == 0 || raw == "-" ? null : raw; } private static DateOnly? ParseDate(string raw) { return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null; } }