using System.Xml.Linq;
namespace Infrastructure.StockData;
///
/// SEIBro OpenAPI 공용 응답 envelope 파서 — 순수 C# (System.Xml.Linq), 37개 API 전 공통 (docs/SEIBro/readme.md §1 확정 규칙).
/// 응답 골격:
/// <vector result="N"> ← result 속성 = 행 수 (0 = 빈 결과, data 없음)
/// <data vectorkey="i" type="Document">
/// <result> 또는 <RESULT> ← ⚠️ 래퍼 대소문자가 API 개별로 다름 (파생결합 5종만 대문자)
/// <FIELD value="..."/> ← 필드 값은 element 텍스트가 아니라 value 속성
/// </result>
/// </data>
/// </vector>
/// 파서 규칙:
/// • 래퍼는 data 의 "첫 자식 element" 를 이름·대소문자 무관하게 취한다 (result/RESULT 둘 다 실재).
/// • value="" 또는 element 자체 생략 = null → 행 딕셔너리에서 키 자체를 뺀다 (조회는 GetOrNull 사용).
/// • 필드명 조회는 대소문자 무시 (OrdinalIgnoreCase).
/// • 비XML 응답은 XmlException, envelope 구조 위반(vector/result 속성/래퍼 부재)은 FormatException —
/// silent 빈 결과 금지 (수집 배치가 오류를 인지하고 run 을 중단·재시도해야 한다).
///
public static class SeibroXml
{
/// 파싱된 envelope — ResultCount 는 vector@result 원문 값 (샘플 파일은 truncate 되어 Rows.Count 와 다를 수 있음)
public sealed record Envelope(int ResultCount, IReadOnlyList> Rows);
public static Envelope Parse(string xml)
{
var doc = XDocument.Parse(xml);
var root = doc.Root;
// 라이브 응답은 … 로 를 감싼다.
// (샘플 조각은 단독) → 루트가 vector 면 그대로, 아니면 자손에서 를 찾는다.
var vector = root is not null && root.Name.LocalName.Equals("vector", StringComparison.OrdinalIgnoreCase)
? root
: root?.Descendants().FirstOrDefault(c => c.Name.LocalName.Equals("vector", StringComparison.OrdinalIgnoreCase));
if (vector is null)
{
throw new FormatException($"SEIBro 응답에 없음: 루트 <{root?.Name.LocalName}>");
}
var resultAttr = vector.Attributes().FirstOrDefault(c => c.Name.LocalName.Equals("result", StringComparison.OrdinalIgnoreCase));
if (resultAttr is null || !int.TryParse(resultAttr.Value, out var resultCount))
{
throw new FormatException($"SEIBro 응답 에 result 행수 속성 없음/비숫자: '{resultAttr?.Value}'");
}
var rows = new List>();
var dataIndex = 0;
foreach (var data in vector.Elements().Where(c => c.Name.LocalName.Equals("data", StringComparison.OrdinalIgnoreCase)))
{
// 행 래퍼 = data 의 첫 자식 element (이름·대소문자 무관 — result/RESULT 혼재)
var wrapper = data.Elements().FirstOrDefault() ?? throw new FormatException($"SEIBro 응답 data[{dataIndex}] 에 행 래퍼 element 없음");
var fields = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var field in wrapper.Elements())
{
var value = field.Attributes().FirstOrDefault(c => c.Name.LocalName.Equals("value", StringComparison.OrdinalIgnoreCase))?.Value;
// value="" 는 null 취급 → 키 자체를 넣지 않는다 (element 생략과 동일 의미)
if (!string.IsNullOrEmpty(value))
{
fields[field.Name.LocalName] = value;
}
}
rows.Add(fields);
dataIndex++;
}
return new Envelope(resultCount, rows);
}
/// 행 딕셔너리에서 필드 조회 — 생략/빈 값 필드는 null (파서 공용 헬퍼)
public static string? GetOrNull(IReadOnlyDictionary row, string field)
{
return row.TryGetValue(field, out var value) ? value : null;
}
}