초기 커밋: 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
@@ -0,0 +1,233 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using SheetMe.Core.Models;
namespace SheetMe.Core.Serialization;
/// <summary>
/// JSON 직렬화기 — 부 포맷(파일 내보내기/백업, 추후 주 포맷 승격 대비).
/// 모델 트리를 있는 그대로 담아 레거시 XML 과 동일한 정보량을 유지한다.
/// </summary>
public sealed class FormJsonSerializer : IDesignSerializer
{
#region Member Fields
private static readonly JsonSerializerOptions options = CreateOptions();
#endregion
#region Methods
/// <summary>JSON 을 서식 문서로 역직렬화</summary>
public FormDocument Read(string content)
=> JsonSerializer.Deserialize<FormDocument>(content, options)
?? throw new InvalidDataException("JSON 서식 문서를 해석할 수 없습니다.");
/// <summary>서식 문서를 JSON 으로 직렬화</summary>
public string Write(FormDocument document)
=> JsonSerializer.Serialize(document, options);
private static JsonSerializerOptions CreateOptions()
{
var opts = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
opts.Converters.Add(new PropBagJsonConverter());
opts.Converters.Add(new LegacyPropValueJsonConverter());
return opts;
}
#endregion
}
/// <summary>PropBag JSON 컨버터 — 순서 보존을 위해 [{ "n": 이름, "v": 값 }] 배열로 직렬화</summary>
public sealed class PropBagJsonConverter : JsonConverter<PropBag>
{
public override PropBag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var bag = new PropBag();
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("PropBag 은 배열이어야 합니다.");
}
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("PropBag 항목은 객체여야 합니다.");
}
string? name = null;
LegacyPropValue? value = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var propName = reader.GetString();
reader.Read();
switch (propName)
{
case "n":
name = reader.GetString();
break;
case "v":
value = JsonSerializer.Deserialize<LegacyPropValue>(ref reader, options);
break;
default:
reader.Skip();
break;
}
}
if (name is not null)
{
bag.Set(name, value ?? new LegacyPropValue.NullValue());
}
}
return bag;
}
public override void Write(Utf8JsonWriter writer, PropBag value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var (name, propValue) in value)
{
writer.WriteStartObject();
writer.WriteString("n", name);
writer.WritePropertyName("v");
JsonSerializer.Serialize(writer, propValue, options);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}
/// <summary>LegacyPropValue 다형 JSON 컨버터 — "k" 판별자 기반</summary>
public sealed class LegacyPropValueJsonConverter : JsonConverter<LegacyPropValue>
{
public override LegacyPropValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return new LegacyPropValue.NullValue();
}
if (reader.TokenType == JsonTokenType.String)
{
// 축약형: 문자열이면 TextValue
return new LegacyPropValue.TextValue(reader.GetString() ?? string.Empty);
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("LegacyPropValue 는 문자열 또는 객체여야 합니다.");
}
string? kind = null;
string? text = null;
string? base64 = null;
string? refName = null;
PropBag? nested = null;
List<LegacyItem>? items = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var propName = reader.GetString();
reader.Read();
switch (propName)
{
case "k": kind = reader.GetString(); break;
case "text": text = reader.GetString(); break;
case "b64": base64 = reader.GetString(); break;
case "ref": refName = reader.GetString(); break;
case "props": nested = JsonSerializer.Deserialize<PropBag>(ref reader, options); break;
case "items":
items = new List<LegacyItem>();
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("items 는 배열이어야 합니다.");
}
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
string? aqn = null;
LegacyPropValue? itemValue = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var itemProp = reader.GetString();
reader.Read();
switch (itemProp)
{
case "aqn": aqn = reader.GetString(); break;
case "v": itemValue = JsonSerializer.Deserialize<LegacyPropValue>(ref reader, options); break;
default: reader.Skip(); break;
}
}
items.Add(new LegacyItem { Aqn = aqn ?? string.Empty, Value = itemValue ?? new LegacyPropValue.NullValue() });
}
break;
default: reader.Skip(); break;
}
}
return kind switch
{
"text" => new LegacyPropValue.TextValue(text ?? string.Empty),
"null" => new LegacyPropValue.NullValue(),
"nested" => new LegacyPropValue.NestedValue { Children = nested ?? new PropBag() },
"items" => new LegacyPropValue.ItemsValue { Items = items ?? new List<LegacyItem>() },
"binary" => new LegacyPropValue.BinaryValue { Base64 = base64 ?? string.Empty },
"ref" => new LegacyPropValue.ReferenceValue { Name = refName ?? string.Empty },
_ => throw new JsonException($"알 수 없는 LegacyPropValue 종류: {kind}"),
};
}
public override void Write(Utf8JsonWriter writer, LegacyPropValue value, JsonSerializerOptions options)
{
switch (value)
{
case LegacyPropValue.TextValue text:
// 가장 흔한 형태는 축약(문자열)으로 — JSON 가독성
writer.WriteStringValue(text.Value);
break;
case LegacyPropValue.NullValue:
writer.WriteStartObject();
writer.WriteString("k", "null");
writer.WriteEndObject();
break;
case LegacyPropValue.NestedValue nested:
writer.WriteStartObject();
writer.WriteString("k", "nested");
writer.WritePropertyName("props");
JsonSerializer.Serialize(writer, nested.Children, options);
writer.WriteEndObject();
break;
case LegacyPropValue.ItemsValue items:
writer.WriteStartObject();
writer.WriteString("k", "items");
writer.WritePropertyName("items");
writer.WriteStartArray();
foreach (var item in items.Items)
{
writer.WriteStartObject();
writer.WriteString("aqn", item.Aqn);
writer.WritePropertyName("v");
JsonSerializer.Serialize(writer, item.Value, options);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
break;
case LegacyPropValue.BinaryValue binary:
writer.WriteStartObject();
writer.WriteString("k", "binary");
writer.WriteString("b64", binary.Base64);
writer.WriteEndObject();
break;
case LegacyPropValue.ReferenceValue reference:
writer.WriteStartObject();
writer.WriteString("k", "ref");
writer.WriteString("ref", reference.Name);
writer.WriteEndObject();
break;
}
}
}