WeddingThankYou 1.0.0.0
결혼식 이후 감사 인사와 사진, 계좌 안내, 연락처를 모바일 페이지로 전달하는 감사장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
JsonTenantStore.cs
이 파일의 문서화 페이지로 가기
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Text.Json;
5using System.Text.Json.Serialization;
6using System.Threading;
7using System.Threading.Tasks;
8using Wedding.Common;
10
12
21public sealed class JsonTenantStore : ITenantStore
22{
31 private readonly string _dataRoot;
40 private static readonly SemaphoreSlim _gate = new(1, 1);
41
50 private static readonly JsonSerializerOptions _jsonOpts = new()
51 {
52 WriteIndented = true,
53 PropertyNameCaseInsensitive = true,
54 Converters = { new WeddingLayoutModeJsonConverter(), new JsonStringEnumConverter() }
55 };
56
74 {
76 Directory.CreateDirectory(_dataRoot);
77 }
78
103 public string GetTenantDataPath(string slug) =>
104 Path.Combine(_dataRoot, Sanitize(slug));
105
138 public async Task<TenantConfig?> GetAsync(string slug, CancellationToken ct = default)
139 {
140 var path = ConfigPath(slug);
141 if (!File.Exists(path)) return null;
142
143 await _gate.WaitAsync(ct).ConfigureAwait(false);
144 try
145 {
146 return await ReadTenantAsync(path, ct).ConfigureAwait(false);
147 }
148 finally { _gate.Release(); }
149 }
150
175 public async Task<IReadOnlyList<TenantConfig>> GetAllAsync(CancellationToken ct = default)
176 {
177 var list = new List<TenantConfig>();
178 if (!Directory.Exists(_dataRoot)) return list;
179
180 foreach (var dir in Directory.GetDirectories(_dataRoot))
181 {
182 var cfg = Path.Combine(dir, "config.json");
183 if (!File.Exists(cfg)) continue;
184
185 await _gate.WaitAsync(ct).ConfigureAwait(false);
186 try
187 {
188 var t = await ReadTenantAsync(cfg, ct).ConfigureAwait(false);
189 if (t != null) list.Add(t);
190 }
191 catch (JsonException ex)
192 {
193 LogTenantReadError(cfg, ex);
194 }
195 finally { _gate.Release(); }
196 }
197 return list;
198 }
199
232 public async Task SaveAsync(TenantConfig config, CancellationToken ct = default)
233 {
234 var dir = GetTenantDataPath(config.Slug);
235 Directory.CreateDirectory(dir);
236 Directory.CreateDirectory(Path.Combine(dir, "gallery"));
237 Directory.CreateDirectory(Path.Combine(dir, "thumb"));
238
239 var tmp = ConfigPath(config.Slug) + ".tmp";
240 await _gate.WaitAsync(ct).ConfigureAwait(false);
241 try
242 {
243 await using (var fs = File.Create(tmp))
244 await JsonSerializer.SerializeAsync(fs, config, _jsonOpts, ct).ConfigureAwait(false);
245
246 File.Copy(tmp, ConfigPath(config.Slug), overwrite: true);
247 File.Delete(tmp);
248 }
249 finally { _gate.Release(); }
250 }
251
284 public Task<bool> ExistsAsync(string slug, CancellationToken ct = default) =>
285 Task.FromResult(File.Exists(ConfigPath(slug)));
286
319 public Task DeleteAsync(string slug, CancellationToken ct = default)
320 {
321 var dir = GetTenantDataPath(slug);
322 if (Directory.Exists(dir))
323 Directory.Delete(dir, recursive: true);
324 return Task.CompletedTask;
325 }
326
351 private string ConfigPath(string slug) =>
352 Path.Combine(GetTenantDataPath(slug), "config.json");
353
378 private static string Sanitize(string slug) =>
379 string.Concat(slug.ToLowerInvariant().Split(Path.GetInvalidFileNameChars()));
380
413 private static async Task<TenantConfig?> ReadTenantAsync(string path, CancellationToken ct)
414 {
415 var json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false);
416 LogLegacyLayoutModeFallback(path, json);
417 return JsonSerializer.Deserialize<TenantConfig>(json, _jsonOpts);
418 }
419
444 private static void LogLegacyLayoutModeFallback(string path, string json)
445 {
446 var raw = TryGetDesignLayoutMode(json);
447 if (raw is null || IsCanonicalLayoutMode(raw)) return;
448
449 Console.Error.WriteLine($"[JsonTenantStore] Legacy DesignSettings.LayoutMode '{raw}' in '{path}' was loaded with compatibility fallback.");
450 }
451
476 private static string? TryGetDesignLayoutMode(string json)
477 {
478 try
479 {
480 using var doc = JsonDocument.Parse(json);
481 if (!TryGetProperty(doc.RootElement, "DesignSettings", out var design)) return null;
482 if (!TryGetProperty(design, "LayoutMode", out var layout)) return null;
483
484 return layout.ValueKind switch
485 {
486 JsonValueKind.String => layout.GetString(),
487 JsonValueKind.Null => "",
488 JsonValueKind.Number => layout.GetRawText(),
489 _ => layout.GetRawText(),
490 };
491 }
492 catch (JsonException)
493 {
494 return null;
495 }
496 }
497
538 private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
539 {
540 foreach (var property in element.EnumerateObject())
541 {
542 if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
543 {
544 value = property.Value;
545 return true;
546 }
547 }
548
549 value = default;
550 return false;
551 }
552
577 private static bool IsCanonicalLayoutMode(string? value) =>
578 value is not null
579 && (string.Equals(value, "WebPage", StringComparison.Ordinal)
580 || string.Equals(value, "TabMenu", StringComparison.Ordinal)
581 || string.Equals(value, "Gallery", StringComparison.Ordinal)
582 || string.Equals(value, "Story", StringComparison.Ordinal)
583 || string.Equals(value, "Card", StringComparison.Ordinal)
584 || string.Equals(value, "PhotoBook", StringComparison.Ordinal));
585
610 private static void LogTenantReadError(string path, JsonException ex)
611 {
612 Console.Error.WriteLine($"[JsonTenantStore] Skipped invalid tenant JSON '{path}': {ex.Message}");
613 }
614}
static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
static void LogTenantReadError(string path, JsonException ex)
async Task< TenantConfig?> GetAsync(string slug, CancellationToken ct=default)
async Task SaveAsync(TenantConfig config, CancellationToken ct=default)
static bool IsCanonicalLayoutMode(string? value)
static ? string TryGetDesignLayoutMode(string json)
static readonly JsonSerializerOptions _jsonOpts
Task DeleteAsync(string slug, CancellationToken ct=default)
Task< bool > ExistsAsync(string slug, CancellationToken ct=default)
static void LogLegacyLayoutModeFallback(string path, string json)
static async Task< TenantConfig?> ReadTenantAsync(string path, CancellationToken ct)
static readonly SemaphoreSlim _gate
async Task< IReadOnlyList< TenantConfig > > GetAllAsync(CancellationToken ct=default)