초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너. 기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49. 이 커밋에 함께 포함된 자격증명 분리: - appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환 - 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외, csproj Debug 조건부 복사라 Release 산출물에 실리지 않음) - ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경, 미치환 플레이스홀더는 '미설정'으로 간주 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using SheetMe.Core.Models;
|
||||
|
||||
namespace SheetMe.Core.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// 레거시 서식 XML 직렬화기 — v1 주 포맷.
|
||||
/// 포맷 원본: [003]DesignerHosting\clsBasicDesignerLoader.vb (읽기 LoadObject/ReadProperty·쓰기 WriteObjectDesignInfo),
|
||||
/// 병합 규약: clsCommonLib.Xml_MergeXmlDocument (Sheet 루트 1개에 페이지당 MDesignerHost Object N개).
|
||||
/// 모든 Property 를 PropBag 으로 무손실 왕복하며, 쓰기 시 Bounds 를 Location/Size/LocationOnBase 로 동기화한다.
|
||||
/// </summary>
|
||||
public sealed class LegacyXmlSerializer : IDesignSerializer
|
||||
{
|
||||
#region Methods - Read
|
||||
/// <summary>레거시 XML 을 서식 문서로 역직렬화 — 파싱 경고는 document.Meta.ReadWarnings 에 수집</summary>
|
||||
public FormDocument Read(string content)
|
||||
{
|
||||
var document = new FormDocument();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
document.Pages.Add(CreateEmptyPage(1));
|
||||
return document;
|
||||
}
|
||||
|
||||
// 레거시 로더(clsBasicDesignerLoader)와 동일하게 PreserveWhitespace=false 로 파싱.
|
||||
// CheckCharacters=false: 레거시 데이터에 제어문자(예: Spread 구분자 0x05)가 원문 포함된 사례가 있어
|
||||
// 구식 XmlTextReader(무검사)와 동일하게 관용 처리한다.
|
||||
var xml = LoadLenient(content);
|
||||
|
||||
var root = xml.DocumentElement
|
||||
?? throw new InvalidDataException("서식 XML 에 루트 요소가 없습니다.");
|
||||
|
||||
foreach (XmlNode node in root.ChildNodes)
|
||||
{
|
||||
if (node.NodeType != XmlNodeType.Element || node.Name != "Object")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var pageRoot = ParseObject((XmlElement)node, document.Meta.ReadWarnings);
|
||||
document.Pages.Add(new FormPage { Root = pageRoot });
|
||||
}
|
||||
|
||||
if (document.Pages.Count == 0)
|
||||
{
|
||||
document.Meta.ReadWarnings.Add("페이지(Object) 노드를 찾지 못해 빈 페이지를 생성했습니다.");
|
||||
document.Pages.Add(CreateEmptyPage(1));
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/// <summary>빈 페이지 생성 — 레거시 기본 신규 서식(MDesignerHost 720×856)과 동일</summary>
|
||||
public static FormPage CreateEmptyPage(int pageNumber)
|
||||
{
|
||||
var root = new ControlElement
|
||||
{
|
||||
Type = LegacyTypeCatalog.HostClassName,
|
||||
Id = $"MDesignerHost{pageNumber}",
|
||||
LegacyAqn = LegacyTypeCatalog.HostAqn,
|
||||
Bounds = new LayoutRect { W = 720, H = 856 },
|
||||
HasSize = true,
|
||||
};
|
||||
root.Props.SetText("BackColor", "White");
|
||||
root.Props.SetText("Name", root.Id);
|
||||
root.Props.SetText("Size", "720, 856");
|
||||
return new FormPage { Root = root };
|
||||
}
|
||||
|
||||
private ControlElement ParseObject(XmlElement node, List<string> warnings)
|
||||
{
|
||||
var aqn = node.GetAttribute("type");
|
||||
var nameAttr = node.Attributes["name"];
|
||||
var displaySequenceAttr = node.GetAttribute("DisplaySequence");
|
||||
|
||||
var element = new ControlElement
|
||||
{
|
||||
LegacyAqn = string.IsNullOrEmpty(aqn) ? null : aqn,
|
||||
Type = LegacyTypeCatalog.IsHost(aqn) ? LegacyTypeCatalog.HostClassName : LegacyTypeCatalog.Resolve(aqn),
|
||||
Id = nameAttr?.Value ?? string.Empty,
|
||||
HasNameAttr = nameAttr is not null,
|
||||
HasChildrenAttr = node.Attributes["children"] is not null,
|
||||
DisplaySequence = int.TryParse(displaySequenceAttr, out var seq) ? seq : 0,
|
||||
};
|
||||
|
||||
if (element.Type == LegacyTypeCatalog.PlaceholderType)
|
||||
{
|
||||
warnings.Add($"미지원 컨트롤 타입 보존: {element.Id} ({LegacyTypeCatalog.ShortClassName(aqn)})");
|
||||
}
|
||||
|
||||
foreach (XmlNode child in node.ChildNodes)
|
||||
{
|
||||
if (child.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
switch (child.Name)
|
||||
{
|
||||
case "Object":
|
||||
element.Children.Add(ParseObject((XmlElement)child, warnings));
|
||||
break;
|
||||
case "Property":
|
||||
var propName = ((XmlElement)child).GetAttribute("name");
|
||||
if (propName.Length == 0)
|
||||
{
|
||||
warnings.Add($"이름 없는 Property 무시: {element.Id}");
|
||||
break;
|
||||
}
|
||||
element.Props.Set(propName, ParseValue((XmlElement)child, warnings));
|
||||
break;
|
||||
default:
|
||||
warnings.Add($"알 수 없는 노드 무시: <{child.Name}> in {element.Id}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LiftBounds(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
private LegacyPropValue ParseValue(XmlElement propNode, List<string> warnings)
|
||||
{
|
||||
var elementChildren = propNode.ChildNodes.OfType<XmlElement>().ToList();
|
||||
|
||||
if (elementChildren.Count == 0)
|
||||
{
|
||||
var hasText = propNode.ChildNodes
|
||||
.Cast<XmlNode>()
|
||||
.Any(n => n.NodeType is XmlNodeType.Text or XmlNodeType.CDATA);
|
||||
return hasText
|
||||
? new LegacyPropValue.TextValue(propNode.InnerText)
|
||||
: new LegacyPropValue.NullValue();
|
||||
}
|
||||
|
||||
if (elementChildren.All(e => e.Name == "Property"))
|
||||
{
|
||||
var nested = new LegacyPropValue.NestedValue();
|
||||
foreach (var child in elementChildren)
|
||||
{
|
||||
var childName = child.GetAttribute("name");
|
||||
if (childName.Length == 0)
|
||||
{
|
||||
warnings.Add("이름 없는 중첩 Property 무시");
|
||||
continue;
|
||||
}
|
||||
nested.Children.Set(childName, ParseValue(child, warnings));
|
||||
}
|
||||
return nested;
|
||||
}
|
||||
|
||||
if (elementChildren.All(e => e.Name == "Item"))
|
||||
{
|
||||
var items = new LegacyPropValue.ItemsValue();
|
||||
foreach (var child in elementChildren)
|
||||
{
|
||||
items.Items.Add(new LegacyItem
|
||||
{
|
||||
Aqn = child.GetAttribute("type"),
|
||||
Value = ParseValue(child, warnings),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
if (elementChildren.Count == 1 && elementChildren[0].Name == "Binary")
|
||||
{
|
||||
return new LegacyPropValue.BinaryValue { Base64 = elementChildren[0].InnerText };
|
||||
}
|
||||
|
||||
if (elementChildren.Count == 1 && elementChildren[0].Name == "Reference")
|
||||
{
|
||||
return new LegacyPropValue.ReferenceValue { Name = elementChildren[0].GetAttribute("name") };
|
||||
}
|
||||
|
||||
warnings.Add($"해석 불가 Property 형태(자식: {string.Join(",", elementChildren.Select(e => e.Name).Distinct())}) — 중첩으로 보존 시도");
|
||||
var fallback = new LegacyPropValue.NestedValue();
|
||||
foreach (var child in elementChildren.Where(e => e.Name == "Property"))
|
||||
{
|
||||
var childName = child.GetAttribute("name");
|
||||
if (childName.Length > 0)
|
||||
{
|
||||
fallback.Children.Set(childName, ParseValue(child, warnings));
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static void LiftBounds(ControlElement element)
|
||||
{
|
||||
var location = element.Props.GetText("Location");
|
||||
if (location is not null)
|
||||
{
|
||||
var (x, y) = LegacyFormat.ParsePoint(location);
|
||||
element.Bounds.X = x;
|
||||
element.Bounds.Y = y;
|
||||
element.HasLocation = true;
|
||||
}
|
||||
|
||||
var size = element.Props.GetText("Size");
|
||||
if (size is not null)
|
||||
{
|
||||
var (w, h) = LegacyFormat.ParseSize(size);
|
||||
element.Bounds.W = w;
|
||||
element.Bounds.H = h;
|
||||
element.HasSize = true;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - Write
|
||||
/// <summary>서식 문서를 레거시 XML(병합형 — Sheet 루트에 페이지당 Object)로 직렬화</summary>
|
||||
public string Write(FormDocument document)
|
||||
{
|
||||
var xml = new XmlDocument();
|
||||
var sheet = xml.CreateElement("Sheet");
|
||||
var nameAttr = xml.CreateAttribute("name");
|
||||
nameAttr.Value = "SheetDesignInfo";
|
||||
sheet.Attributes.Append(nameAttr);
|
||||
xml.AppendChild(sheet);
|
||||
|
||||
foreach (var page in document.Pages)
|
||||
{
|
||||
sheet.AppendChild(WriteObject(xml, page.Root, 0, 0, isRoot: true));
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
using (var writer = XmlWriter.Create(builder, new XmlWriterSettings
|
||||
{
|
||||
OmitXmlDeclaration = true,
|
||||
Indent = false,
|
||||
// Entitize: 텍스트 내 CR 을 
 로 보존(레거시 XmlDocument 출력과 동일) — 재파싱 정규화 손실 방지
|
||||
NewLineHandling = NewLineHandling.Entitize,
|
||||
// 레거시 데이터의 제어문자(0x05 등) verbatim 재방출 허용
|
||||
CheckCharacters = false,
|
||||
}))
|
||||
{
|
||||
xml.Save(writer);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>관용 파서 — 문자 검사 없이 XmlDocument 로드(레거시 XmlTextReader 동등)</summary>
|
||||
internal static XmlDocument LoadLenient(string content)
|
||||
{
|
||||
var xml = new XmlDocument();
|
||||
using var reader = XmlReader.Create(new StringReader(content), new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
});
|
||||
xml.Load(reader);
|
||||
return xml;
|
||||
}
|
||||
|
||||
private XmlElement WriteObject(XmlDocument xml, ControlElement element, double absX, double absY, bool isRoot)
|
||||
{
|
||||
SyncBoundsToProps(element, absX, absY, isRoot);
|
||||
|
||||
var node = xml.CreateElement("Object");
|
||||
|
||||
// 어트리뷰트 순서는 레거시 writer 와 동일: type → name → children → DisplaySequence
|
||||
var aqn = element.LegacyAqn ?? LegacyTypeCatalog.AqnOf(element.Type)
|
||||
?? throw new InvalidOperationException($"타입 '{element.Type}' 의 레거시 AQN 을 결정할 수 없습니다: {element.Id}");
|
||||
AppendAttr(xml, node, "type", aqn);
|
||||
|
||||
if (element.HasNameAttr && element.Id.Length > 0)
|
||||
{
|
||||
AppendAttr(xml, node, "name", element.Id);
|
||||
}
|
||||
if (element.HasChildrenAttr)
|
||||
{
|
||||
AppendAttr(xml, node, "children", "Controls");
|
||||
}
|
||||
AppendAttr(xml, node, "DisplaySequence", element.DisplaySequence.ToString());
|
||||
|
||||
// 레거시 writer 규약: 자식 Object 먼저, Property 나중
|
||||
foreach (var child in element.Children)
|
||||
{
|
||||
node.AppendChild(WriteObject(xml, child, absX + child.Bounds.X, absY + child.Bounds.Y, isRoot: false));
|
||||
}
|
||||
|
||||
foreach (var (name, value) in element.Props)
|
||||
{
|
||||
node.AppendChild(WriteProperty(xml, name, value));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounds → Props(Location/Size/LocationOnBase/Name) 동기화 — 레거시 writer 의 재계산 동작과 동일.
|
||||
/// XML 에서 읽어온 컨트롤(LegacyAqn 보유)은 있던 키만 갱신(순수 보존),
|
||||
/// 신규 생성 컨트롤(LegacyAqn 없음)에는 레거시 필수 속성(LocationOnBase/Name — 실샘플 117/117 보유)을 추가한다.
|
||||
/// </summary>
|
||||
private static void SyncBoundsToProps(ControlElement element, double absX, double absY, bool isRoot)
|
||||
{
|
||||
var isNew = element.LegacyAqn is null;
|
||||
|
||||
if (!isRoot)
|
||||
{
|
||||
if (element.HasLocation || element.Bounds.X != 0 || element.Bounds.Y != 0)
|
||||
{
|
||||
element.Props.SetText("Location", LegacyFormat.FormatPair(element.Bounds.X, element.Bounds.Y));
|
||||
element.HasLocation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.HasSize || element.Bounds.W != 0 || element.Bounds.H != 0)
|
||||
{
|
||||
element.Props.SetText("Size", LegacyFormat.FormatPair(element.Bounds.W, element.Bounds.H));
|
||||
element.HasSize = true;
|
||||
}
|
||||
|
||||
// LocationOnBase 는 레거시 writer 가 저장 시마다 재계산(부모 절대좌표 누적)
|
||||
if (element.Props.Contains("LocationOnBase") || (!isRoot && isNew))
|
||||
{
|
||||
element.Props.SetText("LocationOnBase", LegacyFormat.FormatPair(absX, absY));
|
||||
}
|
||||
|
||||
// Name Property 는 name 어트리뷰트와 동일 값 유지(개명 동기화)
|
||||
if (element.Id.Length > 0 && (element.Props.Contains("Name") || isNew))
|
||||
{
|
||||
element.Props.SetText("Name", element.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private XmlElement WriteProperty(XmlDocument xml, string name, LegacyPropValue value)
|
||||
{
|
||||
var node = xml.CreateElement("Property");
|
||||
AppendAttr(xml, node, "name", name);
|
||||
WriteValueInto(xml, node, value);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void WriteValueInto(XmlDocument xml, XmlElement node, LegacyPropValue value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case LegacyPropValue.TextValue text:
|
||||
node.InnerText = text.Value;
|
||||
break;
|
||||
|
||||
case LegacyPropValue.NullValue:
|
||||
break;
|
||||
|
||||
case LegacyPropValue.NestedValue nested:
|
||||
foreach (var (childName, childValue) in nested.Children)
|
||||
{
|
||||
node.AppendChild(WriteProperty(xml, childName, childValue));
|
||||
}
|
||||
break;
|
||||
|
||||
case LegacyPropValue.ItemsValue items:
|
||||
foreach (var item in items.Items)
|
||||
{
|
||||
var itemNode = xml.CreateElement("Item");
|
||||
AppendAttr(xml, itemNode, "type", item.Aqn);
|
||||
WriteValueInto(xml, itemNode, item.Value);
|
||||
node.AppendChild(itemNode);
|
||||
}
|
||||
break;
|
||||
|
||||
case LegacyPropValue.BinaryValue binary:
|
||||
var binaryNode = xml.CreateElement("Binary");
|
||||
binaryNode.InnerText = binary.Base64;
|
||||
node.AppendChild(binaryNode);
|
||||
break;
|
||||
|
||||
case LegacyPropValue.ReferenceValue reference:
|
||||
var referenceNode = xml.CreateElement("Reference");
|
||||
AppendAttr(xml, referenceNode, "name", reference.Name);
|
||||
node.AppendChild(referenceNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendAttr(XmlDocument xml, XmlElement node, string name, string value)
|
||||
{
|
||||
var attr = xml.CreateAttribute(name);
|
||||
attr.Value = value;
|
||||
node.Attributes.Append(attr);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user