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;
}
}