초기 커밋: 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:
Msystech
2026-08-11 17:20:22 +09:00
co-authored by Claude Fable 5
commit 16c07f48dc
102 changed files with 14210 additions and 0 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
using SheetMe.Core.Serialization;
namespace SheetMe.Core.Tests;
/// <summary>
/// FarPoint Spread XML 파서 테스트.
/// 합성 샘플 기반(테스트 DB 의 실물 2건은 셀 없는 기본 골격) — 셀 있는 실서식 확보 시 재검증.
/// </summary>
[TestClass]
public sealed class FarPointSpreadParserTests
{
private const string SyntheticXml = """
<?xml version="1.0" encoding="utf-8"?>
<Spread class="M.EMR.UserControl.Spread" assembly="M.EMR.UserControl">
<Presentation>
<Sheets>
<Sheet index="0">
<AxisModels>
<Row class="FarPoint.Win.Spread.Model.DefaultSheetAxisModel" defaultSize="24" orientation="Vertical" count="500">
<Items>
<Item index="0"><Size>40</Size></Item>
<Item index="2"><Size>60</Size></Item>
</Items>
</Row>
<Column class="FarPoint.Win.Spread.Model.DefaultSheetAxisModel" orientation="Horizontal" count="500">
<Items>
<Item index="-1"><Size>80</Size></Item>
<Item index="1"><Size>120</Size></Item>
</Items>
</Column>
</AxisModels>
<SpanModel class="FarPoint.Win.Spread.Model.DefaultSheetSpanModel">
<CellRange Row="0" Column="0" RowCount="1" ColumnCount="2" />
</SpanModel>
</Sheet>
</Sheets>
</Presentation>
<Data>
<Sheets>
<Sheet index="0">
<DataArea class="FarPoint.Win.Spread.Model.DefaultSheetDataModel" rows="500" columns="500">
<Cell Row="0" Column="0"><Data type="System.String"> </Data></Cell>
<Cell Row="1" Column="0"><Data type="System.String"></Data></Cell>
<Cell Row="1" Column="1"><Data type="System.String">3</Data></Cell>
</DataArea>
</Sheet>
</Sheets>
</Data>
</Spread>
""";
/// <summary>합성 샘플 — 축 크기/기본값/스팬/셀 추출</summary>
[TestMethod]
public void Parse_Synthetic_ExtractsAll()
{
var grid = FarPointSpreadParser.Parse(SyntheticXml);
Assert.IsNotNull(grid);
Assert.AreEqual(24, grid!.DefaultRowHeight, "defaultSize 어트리뷰트");
Assert.AreEqual(80, grid.DefaultColWidth, "index=-1 Item 기본값");
Assert.AreEqual(40, grid.RowHeightOf(0));
Assert.AreEqual(24, grid.RowHeightOf(1), "미지정 행은 기본값");
Assert.AreEqual(60, grid.RowHeightOf(2));
Assert.AreEqual(120, grid.ColWidthOf(1));
Assert.AreEqual(80, grid.ColWidthOf(0));
Assert.AreEqual(1, grid.Spans.Count);
Assert.AreEqual(2, grid.Spans[0].ColSpan);
Assert.AreEqual(3, grid.Cells.Count);
Assert.AreEqual("평가 항목", grid.Cells[0].Text);
Assert.AreEqual(1, grid.Cells[2].Row);
Assert.AreEqual(1, grid.Cells[2].Col);
}
/// <summary>실물 기본 골격(S999) — 예외 없이 파싱, 기본 크기 유지</summary>
[TestMethod]
public void Parse_EmptySkeleton_Succeeds()
{
const string skeleton = """
<Spread class="M.EMR.UserControl.Spread">
<Presentation>
<Sheets><Sheet index="0">
<AxisModels>
<Row class="FarPoint.Win.Spread.Model.DefaultSheetAxisModel" orientation="Vertical" count="500">
<Items><Item index="-1" /></Items>
</Row>
<Column class="FarPoint.Win.Spread.Model.DefaultSheetAxisModel" orientation="Horizontal" count="500">
<Items><Item index="-1" /></Items>
</Column>
</AxisModels>
</Sheet></Sheets>
</Presentation>
<Data><Sheets><Sheet index="0">
<DataArea class="FarPoint.Win.Spread.Model.DefaultSheetDataModel" rows="500" columns="500" />
</Sheet></Sheets></Data>
</Spread>
""";
var grid = FarPointSpreadParser.Parse(skeleton);
Assert.IsNotNull(grid);
Assert.AreEqual(20, grid!.DefaultRowHeight);
Assert.AreEqual(62, grid.DefaultColWidth);
Assert.AreEqual(0, grid.Cells.Count);
}
/// <summary>손상 입력 — null 반환(자리표시 렌더 유지)</summary>
[TestMethod]
public void Parse_Invalid_ReturnsNull()
{
Assert.IsNull(FarPointSpreadParser.Parse("not xml at all <"));
}
}
+43
View File
@@ -0,0 +1,43 @@
using SheetMe.Core.Serialization;
namespace SheetMe.Core.Tests;
/// <summary>JSON 부 포맷 테스트 — 레거시 XML 과의 상호 변환 무손실성.</summary>
[TestClass]
public sealed class FormJsonTests
{
private static string SamplePath
=> Path.Combine(AppContext.BaseDirectory, "Assets", "sheetdesign_sample.xml");
/// <summary>XML → 모델 → JSON → 모델 → XML 전체 왕복 — 의미론 diff 0</summary>
[TestMethod]
public void FullRoundTrip_XmlToJsonToXml_NoSemanticDiff()
{
var original = File.ReadAllText(SamplePath);
var xmlSerializer = new LegacyXmlSerializer();
var jsonSerializer = new FormJsonSerializer();
var document = xmlSerializer.Read(original);
var json = jsonSerializer.Write(document);
var restored = jsonSerializer.Read(json);
var rewrittenXml = xmlSerializer.Write(restored);
var diffs = XmlSemanticDiff.Compare(original, rewrittenXml);
Assert.AreEqual(0, diffs.Count,
"XML→JSON→XML 왕복 의미론 diff 발생:\n" + string.Join("\n", diffs));
}
/// <summary>JSON 이중 직렬화 안정성 — 두 번째 출력이 첫 번째와 동일</summary>
[TestMethod]
public void JsonRoundTrip_IsStable()
{
var xmlSerializer = new LegacyXmlSerializer();
var jsonSerializer = new FormJsonSerializer();
var document = xmlSerializer.Read(File.ReadAllText(SamplePath));
var first = jsonSerializer.Write(document);
var second = jsonSerializer.Write(jsonSerializer.Read(first));
Assert.AreEqual(first, second, "JSON 왕복이 안정적이지 않습니다.");
}
}
+1
View File
@@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -0,0 +1,86 @@
using SheetMe.Core.Serialization;
namespace SheetMe.Core.Tests;
/// <summary>레거시 invariant 문자열 포맷 도우미 테스트 — 실샘플 원문 형식과의 일치 검증.</summary>
[TestClass]
public sealed class LegacyFormatTests
{
/// <summary>Point/Size 파싱·생성 — "336, 1064" 왕복</summary>
[TestMethod]
[DataRow("336, 1064", 336, 1064)]
[DataRow("0, 22", 0, 22)]
[DataRow("-5, 10", -5, 10)]
public void Point_RoundTrip(string text, int x, int y)
{
var (px, py) = LegacyFormat.ParsePoint(text);
Assert.AreEqual(x, px);
Assert.AreEqual(y, py);
Assert.AreEqual(text.Replace(" ", "").Insert(text.Replace(" ", "").IndexOf(',') + 1, " "),
LegacyFormat.FormatPair(px, py));
}
/// <summary>폰트 파싱 — 실샘플 원문 3형태</summary>
[TestMethod]
public void Font_Parse_SampleForms()
{
var font1 = LegacyFormat.ParseFont("굴림, 11.25pt, style=Bold");
Assert.AreEqual("굴림", font1.Family);
Assert.AreEqual(11.25, font1.SizePt);
Assert.IsTrue(font1.Bold);
Assert.IsFalse(font1.Italic);
var font2 = LegacyFormat.ParseFont("굴림, 11.25pt");
Assert.IsFalse(font2.Bold);
var font3 = LegacyFormat.ParseFont("돋움, 26.25pt, style=Bold, Underline");
Assert.AreEqual("돋움", font3.Family);
Assert.IsTrue(font3.Bold);
Assert.IsTrue(font3.Underline);
}
/// <summary>폰트 생성 — 레거시 FontConverter invariant 형식과 동일</summary>
[TestMethod]
public void Font_Format_MatchesLegacy()
{
Assert.AreEqual("굴림, 11.25pt, style=Bold",
LegacyFormat.FormatFont(new LegacyFont { Family = "굴림", SizePt = 11.25, Bold = true }));
Assert.AreEqual("굴림, 9pt",
LegacyFormat.FormatFont(new LegacyFont { Family = "굴림", SizePt = 9 }));
Assert.AreEqual("돋움, 26.25pt, style=Bold, Underline",
LegacyFormat.FormatFont(new LegacyFont { Family = "돋움", SizePt = 26.25, Bold = true, Underline = true }));
}
/// <summary>폰트 왕복 — 파싱→생성 원문 유지</summary>
[TestMethod]
[DataRow("굴림, 11.25pt, style=Bold")]
[DataRow("굴림, 11.25pt")]
[DataRow("돋움, 26.25pt, style=Bold")]
public void Font_RoundTrip(string original)
{
Assert.AreEqual(original, LegacyFormat.FormatFont(LegacyFormat.ParseFont(original)));
}
/// <summary>색 파싱 — RGB 성분/명명색/시스템색</summary>
[TestMethod]
public void Color_Parse_SampleForms()
{
Assert.AreEqual(((byte)255, (byte)224, (byte)224, (byte)224), LegacyFormat.ParseColor("224, 224, 224"));
Assert.AreEqual(((byte)255, (byte)255, (byte)255, (byte)255), LegacyFormat.ParseColor("White"));
var transparent = LegacyFormat.ParseColor("Transparent");
Assert.AreEqual(0, transparent.A);
// 시스템색 — 성분값은 OS 테마에 따라 다르므로 해석 성공 여부만 확인
var system = LegacyFormat.ParseColor("ControlDarkDark");
Assert.IsTrue(system.A > 0, "시스템색 해석 실패");
}
/// <summary>색 생성 — "R, G, B" invariant 형식</summary>
[TestMethod]
public void Color_Format_MatchesLegacy()
{
Assert.AreEqual("224, 224, 224", LegacyFormat.FormatColor(255, 224, 224, 224));
Assert.AreEqual("128, 255, 0, 0", LegacyFormat.FormatColor(128, 255, 0, 0));
}
}
@@ -0,0 +1,69 @@
using SheetMe.Core.Catalog;
using SheetMe.Core.Serialization;
namespace SheetMe.Core.Tests;
/// <summary>레거시 타입 매핑 카탈로그 테스트.</summary>
[TestClass]
public sealed class LegacyTypeCatalogTests
{
/// <summary>레거시 클래스 → 중립 타입 매핑 (변형 포함)</summary>
[TestMethod]
[DataRow("M.EMR.UserControl.TextBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "TextBox")]
[DataRow("M.EMR.UserControl.MaskedTextBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "MaskedTextBox")]
[DataRow("M.EMR.UserControl.MFormatLabel, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Label")]
[DataRow("M.EMR.UserControl.MSequence, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Label")]
[DataRow("M.EMR.UserControl.CheckLabel, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "CheckBox")]
[DataRow("M.EMR.UserControl.MCheckedListBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "CheckList")]
[DataRow("M.EMR.UserControl.Panel2, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Panel")]
[DataRow("M.EMR.UserControl.MExpandablePanel, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Panel")]
[DataRow("M.EMR.UserControl.MLine, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Line")]
[DataRow("M.EMR.UserControl.MPictureBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "PictureBox")]
[DataRow("M.EMR.UserControl.MCalcBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "CalcBox")]
public void Resolve_MapsLegacyTypes(string aqn, string expected)
{
Assert.AreEqual(expected, LegacyTypeCatalog.Resolve(aqn));
}
/// <summary>미지원 타입 → Placeholder</summary>
[TestMethod]
[DataRow("M.EMR.UserControl.MSign, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
[DataRow("M.EMR.UserControl.MFile, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
[DataRow("Some.Unknown.CustomControl, Some.Assembly")]
public void Resolve_UnknownTypes_ReturnPlaceholder(string aqn)
{
Assert.AreEqual(LegacyTypeCatalog.PlaceholderType, LegacyTypeCatalog.Resolve(aqn));
}
/// <summary>네이티브 지원 승격 타입 — MButton/MDataTable/Spread(렌더 전용)</summary>
[TestMethod]
[DataRow("M.EMR.UserControl.MButton, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Button")]
[DataRow("M.EMR.UserControl.MDataTable, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "DataTable")]
[DataRow("M.EMR.UserControl.Spread, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Spread")]
public void Resolve_NativeUpgradedTypes(string aqn, string expected)
{
Assert.AreEqual(expected, LegacyTypeCatalog.Resolve(aqn));
}
/// <summary>팔레트 14종 전부 신규 생성 AQN 보유 + 왕복 매핑 일관성</summary>
[TestMethod]
public void AqnOf_AllPaletteTypes_RoundTripConsistent()
{
foreach (var descriptor in ControlRegistry.All)
{
var aqn = LegacyTypeCatalog.AqnOf(descriptor.Type);
Assert.IsNotNull(aqn, $"{descriptor.Type} 의 AQN 이 없습니다.");
Assert.AreEqual(descriptor.Type, LegacyTypeCatalog.Resolve(aqn!),
$"{descriptor.Type} 의 AQN({aqn})이 자기 자신으로 해석되지 않습니다.");
StringAssert.Contains(aqn!, "M.EMR.UserControl", $"{descriptor.Type} AQN 어셈블리 누락");
}
}
/// <summary>MDesignerHost 판정</summary>
[TestMethod]
public void IsHost_DetectsDesignerHost()
{
Assert.IsTrue(LegacyTypeCatalog.IsHost(LegacyTypeCatalog.HostAqn));
Assert.IsFalse(LegacyTypeCatalog.IsHost("M.EMR.UserControl.TextBox, M.EMR.UserControl"));
}
}
@@ -0,0 +1,165 @@
using SheetMe.Core.Models;
using SheetMe.Core.Serialization;
namespace SheetMe.Core.Tests;
/// <summary>
/// 레거시 XML 왕복 무결성 테스트 — P0 게이트.
/// 실서식 샘플(sheetdesign_sample.xml)을 Read→Write 했을 때 의미론 diff 가 0 이어야 한다.
/// </summary>
[TestClass]
public sealed class LegacyXmlRoundTripTests
{
private static string SamplePath
=> Path.Combine(AppContext.BaseDirectory, "Assets", "sheetdesign_sample.xml");
/// <summary>실서식 샘플 왕복 — 의미론 diff 0</summary>
[TestMethod]
public void RoundTrip_RealSample_NoSemanticDiff()
{
var original = File.ReadAllText(SamplePath);
var serializer = new LegacyXmlSerializer();
var document = serializer.Read(original);
var rewritten = serializer.Write(document);
var diffs = XmlSemanticDiff.Compare(original, rewritten);
Assert.AreEqual(0, diffs.Count,
"왕복 의미론 diff 발생:\n" + string.Join("\n", diffs));
}
/// <summary>실서식 샘플 이중 왕복 — Write 출력이 안정(고정점)이어야 한다</summary>
[TestMethod]
public void RoundTrip_RealSample_SecondPassIsStable()
{
var original = File.ReadAllText(SamplePath);
var serializer = new LegacyXmlSerializer();
var first = serializer.Write(serializer.Read(original));
var second = serializer.Write(serializer.Read(first));
Assert.AreEqual(first, second, "이중 왕복 결과가 문자열 수준에서 안정적이지 않습니다.");
}
/// <summary>실서식 샘플 구조 확인 — 페이지/컨트롤 수·타입 매핑</summary>
[TestMethod]
public void Read_RealSample_ParsesStructure()
{
var serializer = new LegacyXmlSerializer();
var document = serializer.Read(File.ReadAllText(SamplePath));
Assert.AreEqual(1, document.Pages.Count, "샘플은 단일 페이지여야 합니다.");
var page = document.Pages[0];
Assert.AreEqual("MDesignerHost1", page.Root.Id);
Assert.AreEqual(823, page.Width, "루트 Size 리프트(823×1250) 실패");
Assert.AreEqual(1250, page.Height);
// 샘플 실측: MLine 34, TextBox 33, Label 27, RadioButton 15, Panel 6, MSequence 1, MPictureBox 1
var allControls = Flatten(page.Controls).ToList();
Assert.AreEqual(117, allControls.Count, "전체 컨트롤 수(루트 제외) 불일치");
Assert.AreEqual(34, allControls.Count(c => c.Type == "Line"));
Assert.AreEqual(33, allControls.Count(c => c.Type == "TextBox"));
Assert.AreEqual(28, allControls.Count(c => c.Type == "Label"), "Label 27 + MSequence 1 = 28");
Assert.AreEqual(15, allControls.Count(c => c.Type == "RadioButton"));
Assert.AreEqual(6, allControls.Count(c => c.Type == "Panel"));
Assert.AreEqual(1, allControls.Count(c => c.Type == "PictureBox"));
Assert.AreEqual(0, allControls.Count(c => c.Type == LegacyTypeCatalog.PlaceholderType),
"샘플에는 미지원 타입이 없어야 합니다.");
}
/// <summary>다중 페이지 병합 규약 — Sheet 루트 1개에 MDesignerHost Object N개</summary>
[TestMethod]
public void RoundTrip_MultiPage_PreservesPages()
{
const string xml =
"<Sheet name=\"SheetDesignInfo\">" +
"<Object type=\"M.EMR.UserControl.MDesignerHost, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" name=\"MDesignerHost1\" children=\"Controls\" DisplaySequence=\"0\">" +
"<Property name=\"Size\">720, 856</Property>" +
"</Object>" +
"<Object type=\"M.EMR.UserControl.MDesignerHost, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" name=\"MDesignerHost1\" children=\"Controls\" DisplaySequence=\"0\">" +
"<Property name=\"Size\">720, 856</Property>" +
"</Object>" +
"</Sheet>";
var serializer = new LegacyXmlSerializer();
var document = serializer.Read(xml);
Assert.AreEqual(2, document.Pages.Count);
var rewritten = serializer.Write(document);
var diffs = XmlSemanticDiff.Compare(xml, rewritten);
Assert.AreEqual(0, diffs.Count, string.Join("\n", diffs));
}
/// <summary>신규 컨트롤 추가 후 쓰기 — Location/Size/AQN 이 레거시 형식으로 생성</summary>
[TestMethod]
public void Write_NewControl_EmitsLegacyFormat()
{
var serializer = new LegacyXmlSerializer();
var document = new FormDocument();
var page = LegacyXmlSerializer.CreateEmptyPage(1);
document.Pages.Add(page);
var textBox = new ControlElement
{
Type = "TextBox",
Id = "TextBox1",
Bounds = new LayoutRect { X = 120, Y = 88, W = 200, H = 24 },
};
textBox.Props.SetText("Text", "환자명");
page.Controls.Add(textBox);
var xml = serializer.Write(document);
StringAssert.Contains(xml, "M.EMR.UserControl.TextBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
StringAssert.Contains(xml, "<Property name=\"Location\">120, 88</Property>");
StringAssert.Contains(xml, "<Property name=\"Size\">200, 24</Property>");
StringAssert.Contains(xml, "<Property name=\"Text\">환자명</Property>");
// 재읽기 시 동일 구조
var reread = serializer.Read(xml);
Assert.AreEqual(1, reread.Pages[0].Controls.Count);
Assert.AreEqual("TextBox", reread.Pages[0].Controls[0].Type);
Assert.AreEqual(120, reread.Pages[0].Controls[0].Bounds.X);
}
/// <summary>컨테이너 중첩 시 LocationOnBase 재계산 — 절대좌표(부모 누적) 규약</summary>
[TestMethod]
public void Write_NestedControl_RecomputesLocationOnBase()
{
const string xml =
"<Sheet name=\"SheetDesignInfo\">" +
"<Object type=\"M.EMR.UserControl.MDesignerHost, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" name=\"MDesignerHost1\" children=\"Controls\" DisplaySequence=\"0\">" +
"<Object type=\"M.EMR.UserControl.Panel, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" name=\"Panel1\" children=\"Controls\" DisplaySequence=\"0\">" +
"<Object type=\"M.EMR.UserControl.TextBox, M.EMR.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" name=\"TextBox1\" children=\"Controls\" DisplaySequence=\"0\">" +
"<Property name=\"LocationOnBase\">0, 0</Property>" +
"<Property name=\"Location\">10, 20</Property>" +
"<Property name=\"Size\">100, 22</Property>" +
"</Object>" +
"<Property name=\"Location\">50, 60</Property>" +
"<Property name=\"Size\">200, 100</Property>" +
"</Object>" +
"<Property name=\"Size\">720, 856</Property>" +
"</Object>" +
"</Sheet>";
var serializer = new LegacyXmlSerializer();
var document = serializer.Read(xml);
var rewritten = serializer.Write(document);
// Panel(50,60) 안의 TextBox(10,20) → LocationOnBase = 60, 80 (레거시 writer 재계산 규약)
StringAssert.Contains(rewritten, "<Property name=\"LocationOnBase\">60, 80</Property>");
}
private static IEnumerable<ControlElement> Flatten(IEnumerable<ControlElement> controls)
{
foreach (var control in controls)
{
yield return control;
foreach (var child in Flatten(control.Children))
{
yield return child;
}
}
}
}
@@ -0,0 +1,71 @@
using SheetMe.Data.Stores;
namespace SheetMe.Core.Tests;
/// <summary>E_SctMst 트리 순회(레거시 bzSaveSheetDesignNControlInfo 이식) 테스트.</summary>
[TestClass]
public sealed class SctMstXmlWalkerTests
{
private static string SamplePath
=> Path.Combine(AppContext.BaseDirectory, "Assets", "sheetdesign_sample.xml");
/// <summary>알파벳 시퀀스 — 레거시 GetNextAlphabetSequence 와 동일</summary>
[TestMethod]
[DataRow("A", "B")]
[DataRow("Y", "Z")]
[DataRow("Z", "AA")]
[DataRow("AA", "AB")]
[DataRow("AZ", "BA")]
[DataRow("ZZ", "AAA")]
[DataRow("AAZ", "ABA")]
public void NextAlphabetSequence_MatchesLegacy(string current, string expected)
{
Assert.AreEqual(expected, SctMstXmlWalker.NextAlphabetSequence(current));
}
/// <summary>실샘플 순회 — 전체 Object 수(118)와 루트/자식 규약</summary>
[TestMethod]
public void Walk_RealSample_ProducesLegacyRows()
{
var rows = SctMstXmlWalker.Walk(File.ReadAllText(SamplePath));
Assert.AreEqual(118, rows.Count, "루트(MDesignerHost) 포함 118행이어야 합니다.");
var root = rows[0];
Assert.AreEqual("MDesignerHost1", root.ObjName);
Assert.AreEqual("A".PadRight(15), root.ObjId);
Assert.AreEqual("M.EMR.UserControl.MDesignerHost", root.ObjType);
Assert.AreEqual(string.Empty.PadRight(15), root.ParentId);
// 첫 자식: ObjId = AAA...? 루트 A 의 첫 자식 = A + AA = "AAA"? 아님 — 부모ID+AA = "AAA" 가 아니라 "A"+"AA"="AAA"
var firstChild = rows[1];
Assert.AreEqual("AAA".PadRight(15), firstChild.ObjId, "루트 A 의 첫 자식은 A+AA=AAA");
Assert.AreEqual("A".PadRight(15), firstChild.ParentId);
// 모든 행의 ObjId 는 15자 패딩
Assert.IsTrue(rows.All(r => r.ObjId.Length == 15));
// ObjType 은 AQN 이 아니라 풀 클래스명(콤마 없음)
Assert.IsTrue(rows.All(r => !r.ObjType.Contains(',')));
}
/// <summary>Text Property 가 SctObjTxt 로 추출되는지</summary>
[TestMethod]
public void Walk_ExtractsText()
{
const string xml =
"<Sheet name=\"SheetDesignInfo\">" +
"<Object type=\"M.EMR.UserControl.MDesignerHost, M.EMR.UserControl\" name=\"MDesignerHost1\" children=\"Controls\" DisplaySequence=\"0\">" +
"<Object type=\"M.EMR.UserControl.Label, M.EMR.UserControl\" name=\"lbl1\" children=\"Controls\" DisplaySequence=\"3\">" +
"<Property name=\"Text\">제목입니다</Property>" +
"</Object>" +
"</Object>" +
"</Sheet>";
var rows = SctMstXmlWalker.Walk(xml);
Assert.AreEqual(2, rows.Count);
Assert.AreEqual("제목입니다", rows[1].ObjText);
Assert.AreEqual("3", rows[1].ObjSeq);
Assert.AreEqual("M.EMR.UserControl.Label", rows[1].ObjType);
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>SheetMe.Core.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SheetMe.Core\SheetMe.Core.csproj" />
<ProjectReference Include="..\..\src\SheetMe.Data\SheetMe.Data.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.7.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.7.3" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>