using System.Text.Json; using System.Text.Json.Serialization; using SheetMe.Core.Models; namespace SheetMe.Core.Serialization; /// /// JSON 직렬화기 — 부 포맷(파일 내보내기/백업, 추후 주 포맷 승격 대비). /// 모델 트리를 있는 그대로 담아 레거시 XML 과 동일한 정보량을 유지한다. /// public sealed class FormJsonSerializer : IDesignSerializer { #region Member Fields private static readonly JsonSerializerOptions options = CreateOptions(); #endregion #region Methods /// JSON 을 서식 문서로 역직렬화 public FormDocument Read(string content) => JsonSerializer.Deserialize(content, options) ?? throw new InvalidDataException("JSON 서식 문서를 해석할 수 없습니다."); /// 서식 문서를 JSON 으로 직렬화 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 } /// PropBag JSON 컨버터 — 순서 보존을 위해 [{ "n": 이름, "v": 값 }] 배열로 직렬화 public sealed class PropBagJsonConverter : JsonConverter { 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(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(); } } /// LegacyPropValue 다형 JSON 컨버터 — "k" 판별자 기반 public sealed class LegacyPropValueJsonConverter : JsonConverter { 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? 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(ref reader, options); break; case "items": items = new List(); 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(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() }, "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; } } }