DartDisclosureParser.cs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. using System.Globalization;
  2. using System.Text.Json;
  3. namespace Infrastructure.StockData;
  4. /// <summary>
  5. /// OpenDART 공시검색(list.json) 응답 파서 — 순수 C# (System.Text.Json).
  6. /// 응답 골격: { "status", "message", "page_no", "page_count", "total_count", "total_page", "list":[ {...} ] }.
  7. /// status: "000" 정상, "013" 조회된 데이터 없음, "020"/"100" 등은 키/파라미터 오류. list 항목의 모든 값은 문자열.
  8. /// - stock_code 는 6자리 단축코드(상장) 또는 "" (비상장) → 빈 값은 null.
  9. /// - rcept_dt 는 YYYYMMDD → DateOnly. rm(비고)은 없거나 "" → null.
  10. /// list.json 이 stock_code 를 이미 포함하므로 corpCode.xml 매핑 없이 종목별 조회가 가능하다.
  11. /// </summary>
  12. public static class DartDisclosureParser
  13. {
  14. /// <summary>공시 1행 (list.json 항목). stock_code 빈 값은 null, rcept_dt 는 DateOnly.</summary>
  15. public sealed record DisclosureRow(
  16. string RceptNo,
  17. string CorpCode,
  18. string CorpName,
  19. string? StockCode,
  20. string CorpCls,
  21. string ReportNm,
  22. string FlrNm,
  23. DateOnly RceptDt,
  24. string? Rm
  25. );
  26. /// <summary>파싱 결과 — status/총페이지 등 메타와 파싱된 행. status != "000" 이면 Rows 는 빈 목록.</summary>
  27. public sealed record ParseResult(
  28. string Status,
  29. string Message,
  30. int PageNo,
  31. int TotalPage,
  32. int TotalCount,
  33. IReadOnlyList<DisclosureRow> Rows
  34. );
  35. public static ParseResult Parse(string json)
  36. {
  37. using var doc = JsonDocument.Parse(json);
  38. var root = doc.RootElement;
  39. var status = GetString(root, "status");
  40. var message = GetString(root, "message");
  41. var pageNo = GetInt(root, "page_no");
  42. var totalPage = GetInt(root, "total_page");
  43. var totalCount = GetInt(root, "total_count");
  44. var rows = new List<DisclosureRow>();
  45. if (status != "000" || !root.TryGetProperty("list", out var list) || list.ValueKind != JsonValueKind.Array)
  46. {
  47. return new ParseResult(status, message, pageNo, totalPage, totalCount, rows);
  48. }
  49. foreach (var item in list.EnumerateArray())
  50. {
  51. var rceptNo = GetString(item, "rcept_no");
  52. var corpCode = GetString(item, "corp_code");
  53. var reportNm = GetString(item, "report_nm");
  54. var rceptDt = ParseDate(GetString(item, "rcept_dt"));
  55. if (rceptNo.Length == 0 || corpCode.Length == 0 || reportNm.Length == 0 || rceptDt is null)
  56. {
  57. continue;
  58. }
  59. rows.Add(new DisclosureRow(
  60. rceptNo,
  61. corpCode,
  62. GetString(item, "corp_name"),
  63. ToNull(GetString(item, "stock_code")),
  64. GetString(item, "corp_cls"),
  65. reportNm,
  66. GetString(item, "flr_nm"),
  67. rceptDt.Value,
  68. ToNull(GetString(item, "rm"))
  69. ));
  70. }
  71. return new ParseResult(status, message, pageNo, totalPage, totalCount, rows);
  72. }
  73. private static string GetString(JsonElement element, string name)
  74. {
  75. if (!element.TryGetProperty(name, out var value))
  76. {
  77. return string.Empty;
  78. }
  79. return value.ValueKind switch
  80. {
  81. JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
  82. JsonValueKind.Number => value.GetRawText(),
  83. _ => string.Empty
  84. };
  85. }
  86. private static int GetInt(JsonElement element, string name)
  87. {
  88. if (!element.TryGetProperty(name, out var value))
  89. {
  90. return 0;
  91. }
  92. return value.ValueKind switch
  93. {
  94. JsonValueKind.Number => value.TryGetInt32(out var n) ? n : 0,
  95. JsonValueKind.String => int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var s) ? s : 0,
  96. _ => 0
  97. };
  98. }
  99. /// <summary>빈 문자열/"-" 은 null, 나머지는 trim 값</summary>
  100. private static string? ToNull(string raw)
  101. {
  102. return raw.Length == 0 || raw == "-" ? null : raw;
  103. }
  104. private static DateOnly? ParseDate(string raw)
  105. {
  106. return DateOnly.TryParseExact(raw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
  107. }
  108. }