레거시 서식생성기(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>
234 lines
9.2 KiB
C#
234 lines
9.2 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|