| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- using System.Globalization;
- using System.Text.Json;
- namespace Infrastructure.StockData;
- /// <summary>
- /// OpenDART 공시검색(list.json) 응답 파서 — 순수 C# (System.Text.Json).
- /// 응답 골격: { "status", "message", "page_no", "page_count", "total_count", "total_page", "list":[ {...} ] }.
- /// status: "000" 정상, "013" 조회된 데이터 없음(정상 빈결과), "010"/"011" 키 오류, "020" 요청 제한(쿼터) 초과,
- /// "100" 파라미터 오류 등 — 전체 라벨은 DescribeStatus 참고. list 항목의 모든 값은 문자열.
- /// - stock_code 는 6자리 단축코드(상장) 또는 "" (비상장) → 빈 값은 null.
- /// - rcept_dt 는 YYYYMMDD → DateOnly. rm(비고)은 없거나 "" → null.
- /// list.json 이 stock_code 를 이미 포함하므로 corpCode.xml 매핑 없이 종목별 조회가 가능하다.
- /// </summary>
- public static class DartDisclosureParser
- {
- /// <summary>공시 1행 (list.json 항목). stock_code 빈 값은 null, rcept_dt 는 DateOnly.</summary>
- public sealed record DisclosureRow(
- string RceptNo,
- string CorpCode,
- string CorpName,
- string? StockCode,
- string CorpCls,
- string ReportNm,
- string FlrNm,
- DateOnly RceptDt,
- string? Rm
- );
- /// <summary>파싱 결과 — status/총페이지 등 메타와 파싱된 행. status != "000" 이면 Rows 는 빈 목록.</summary>
- public sealed record ParseResult(
- string Status,
- string Message,
- int PageNo,
- int TotalPage,
- int TotalCount,
- IReadOnlyList<DisclosureRow> 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<DisclosureRow>();
- 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);
- }
- /// <summary>
- /// OpenDART 공통 status 코드 라벨 (개발가이드 메시지 설명 기준).
- /// 주의: "020" 은 '요청 제한(쿼터) 초과'다 — 키 오류("010"/"011")와 혼동 금지 (일 20,000건 한도).
- /// </summary>
- public static string DescribeStatus(string status)
- {
- return status switch
- {
- "000" => "정상",
- "010" => "등록되지 않은 키",
- "011" => "사용할 수 없는 키 (사용중지/삭제)",
- "012" => "접근할 수 없는 IP",
- "013" => "조회된 데이터 없음",
- "014" => "파일이 존재하지 않음",
- "020" => "요청 제한(쿼터) 초과",
- "021" => "조회 가능한 회사 개수 초과 (최대 100건)",
- "100" => "필드의 부적절한 값 (파라미터 오류)",
- "101" => "부적절한 접근",
- "800" => "시스템 점검으로 서비스 중지",
- "900" => "정의되지 않은 오류",
- "901" => "계정 개인정보 보유기간 만료로 사용할 수 없는 키",
- _ => "알 수 없는 상태"
- };
- }
- 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>빈 문자열/"-" 은 null, 나머지는 trim 값</summary>
- 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;
- }
- }
|