초기 커밋: 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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user