WeddingThankYou 1.0.0.0
결혼식 이후 감사 인사와 사진, 계좌 안내, 연락처를 모바일 페이지로 전달하는 감사장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
SuperAdminViewModel.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using Dreamine.Identity;
5using Wedding.Common;
6
8
17public sealed class SuperAdminViewModel
18{
27 private readonly ITenantStore _tenants;
36 private readonly WeddingOptions _opts;
82
148 ITenantStore tenants,
149 WeddingOptions opts,
150 IGlobalSettingsStore globalSettings,
151 ThankYouUserContext userContext,
152 IMediaUsageQueryService mediaUsage,
153 IMediaMigrationService mediaMigration,
154 ISuperAdminSessionTokenService superAdminTokens)
155 {
156 _tenants = tenants;
157 _opts = opts;
158 _globalSettings = globalSettings;
159 _userContext = userContext;
160 _mediaUsage = mediaUsage;
161 _mediaMigration = mediaMigration;
162 _superAdminTokens = superAdminTokens;
163 }
164
173 public int MaxVideoSizeMb { get; set; } = 50;
182 public int MaxVideoCount { get; set; } = 1;
200 public IReadOnlyDictionary<string, TenantMediaUsageSummary> MediaUsage { get; private set; }
201 = new Dictionary<string, TenantMediaUsageSummary>(StringComparer.OrdinalIgnoreCase);
210 public bool IsMediaUsageAccurate { get; private set; }
219 public IReadOnlyDictionary<string, MediaMigrationTenantStatus> MigrationStatuses { get; private set; }
220 = new Dictionary<string, MediaMigrationTenantStatus>(StringComparer.OrdinalIgnoreCase);
221
246 public async Task LoadGlobalSettingsAsync(CancellationToken ct = default)
247 {
248 var settings = await _globalSettings.GetAsync(ct).ConfigureAwait(false);
249 settings.Normalize();
250 MediaPolicy = settings.MediaPolicy;
251 MaxVideoSizeMb = MediaPolicy.FreeTier.VideoMaxFileSizeMb;
252 MaxVideoCount = MediaPolicy.FreeTier.VideoMaxCount;
253 }
254
279 public async Task SaveGlobalSettingsAsync(CancellationToken ct = default)
280 {
281 MediaPolicy.Normalize();
282 MaxVideoSizeMb = MediaPolicy.FreeTier.VideoMaxFileSizeMb;
283 MaxVideoCount = MediaPolicy.FreeTier.VideoMaxCount;
284 await _globalSettings.SaveAsync(
286 {
290 }, ct)
291 .ConfigureAwait(false);
292 StatusMessage = "✅ 전체 설정이 저장되었습니다.";
293 }
294
303 public bool IsAuthenticated { get; private set; }
312 public string LoginPassword { get; set; } = "";
321 public string SuperAdminSessionToken { get; private set; } = "";
322
339 public Task<bool> LoginAsync()
340 {
341 IsAuthenticated = DreaminePasswordHasher.VerifyPassword(LoginPassword, _opts.SuperAdminPassword);
342 StatusMessage = IsAuthenticated ? "" : "비밀번호가 틀렸습니다.";
344 return Task.FromResult(IsAuthenticated);
345 }
346
355 public void RestoreSession()
356 {
357 IsAuthenticated = true;
358 StatusMessage = "";
359 }
360
369 public IReadOnlyList<TenantConfig> Tenants { get; private set; } = [];
378 public string StatusMessage { get; private set; } = "";
387 public bool IsLoaded { get; private set; }
388
397 public string NewSlug { get; set; } = "";
406 public string NewCoupleName { get; set; } = "";
415 public string NewPassword { get; set; } = "";
424 public DateTime NewWeddingDate { get; set; } = DateTime.Today.AddMonths(3);
425
450 public async Task LoadAsync(CancellationToken ct = default)
451 {
452 await ReloadTenantsAsync(ct).ConfigureAwait(false);
453 await LoadGlobalSettingsAsync(ct).ConfigureAwait(false);
454 MigrationStatuses = await _mediaMigration.GetAllAsync(ct).ConfigureAwait(false);
456 IsMediaUsageAccurate = false;
457 IsLoaded = true;
458 StatusMessage = "";
459 }
460
485 public async Task RefreshMediaUsageAsync(CancellationToken ct = default)
486 {
487 MigrationStatuses = await _mediaMigration.GetAllAsync(ct).ConfigureAwait(false);
488 MediaUsage = await _mediaUsage.GetTenantUsageAsync(Tenants, ct).ConfigureAwait(false);
490 }
491
516 private async Task ReloadTenantsAsync(CancellationToken ct)
517 {
518 Tenants = await _tenants.GetAllAsync(ct).ConfigureAwait(false);
519 foreach (var tenant in Tenants)
520 {
522 }
523 }
524
541 private IReadOnlyDictionary<string, TenantMediaUsageSummary> BuildFastMediaUsage()
542 {
543 var result = new Dictionary<string, TenantMediaUsageSummary>(StringComparer.OrdinalIgnoreCase);
544 foreach (var tenant in Tenants)
545 {
546 if (MediaUsage.TryGetValue(tenant.Slug, out var existing))
547 {
548 existing.ImageCount = tenant.GalleryFileNames.Count;
549 existing.VideoCount = tenant.VideoFileNames.Count;
550 existing.MigrationState = ResolveMigrationState(tenant);
551 result[tenant.Slug] = existing;
552 continue;
553 }
554
555 result[tenant.Slug] = new TenantMediaUsageSummary
556 {
557 Slug = tenant.Slug,
558 ImageCount = tenant.GalleryFileNames.Count,
559 VideoCount = tenant.VideoFileNames.Count,
560 MigrationState = ResolveMigrationState(tenant),
561 LastModified = tenant.CreatedAt
562 };
563 }
564
565 return result;
566 }
567
593 {
594 if (MigrationStatuses.TryGetValue(tenant.Slug, out var status))
595 {
596 return status.State;
597 }
598
599 if (tenant.GalleryFileNames.Count == 0) return MediaMigrationState.Skipped;
600 return tenant.GalleryFileNames.All(x => Path.GetExtension(x).Equals(".webp", StringComparison.OrdinalIgnoreCase))
602 : MediaMigrationState.Pending;
603 }
604
629 public async Task<bool> CreateTenantAsync(CancellationToken ct = default)
630 {
631 var slug = NewSlug.Trim().ToLowerInvariant();
632
633 if (string.IsNullOrWhiteSpace(slug))
634 { StatusMessage = "슬러그(URL ID)를 입력해주세요."; return false; }
635
636 if (string.IsNullOrWhiteSpace(NewCoupleName))
637 { StatusMessage = "커플 이름을 입력해주세요."; return false; }
638
639 if (string.IsNullOrWhiteSpace(NewPassword))
640 { StatusMessage = "어드민 비밀번호를 입력해주세요."; return false; }
641
642 if (NewPassword.Length < 8)
643 { StatusMessage = "어드민 비밀번호는 8자 이상이어야 합니다."; return false; }
644
645 if (slug is "admin" or "superadmin")
646 { StatusMessage = "'admin/superadmin'은 슬러그로 사용할 수 없습니다."; return false; }
647
648 if (await _tenants.ExistsAsync(slug, ct).ConfigureAwait(false))
649 { StatusMessage = $"이미 존재하는 슬러그입니다: {slug}"; return false; }
650
651 var user = await _userContext.GetCurrentAsync().ConfigureAwait(false);
652 var config = new TenantConfig
653 {
654 Slug = slug,
655 CoupleName = NewCoupleName.Trim(),
656 WeddingDate = NewWeddingDate,
657 PasswordHash = DreaminePasswordHasher.HashPassword(NewPassword),
658 IsPublished = true
659 };
660
661 if (user.IsAuthenticated)
662 {
663 config.OwnerUserId = user.Id;
664 config.OwnerProvider = user.Provider;
665 config.OwnerEmail = user.Email;
666 config.OwnerDisplayName = user.DisplayName;
667 config.OwnerLinkedAt = DateTime.Now;
668 config.AdminUsers.Add(new ThankYouAdminUser
669 {
670 UserId = user.Id,
671 Provider = user.Provider,
672 Email = user.Email,
673 DisplayName = user.DisplayName,
674 Role = "Owner",
675 AddedAt = DateTime.Now
676 });
677 }
678
679 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
680 await LoadAsync(ct).ConfigureAwait(false);
681
682 NewSlug = "";
683 NewCoupleName = "";
684 NewPassword = "";
685 NewWeddingDate = DateTime.Today.AddMonths(3);
686
687 StatusMessage = $"✅ '{slug}' 감사장이 생성되었습니다.";
688 return true;
689 }
690
739 public async Task SetDisplayAsync(string slug, bool showOnHome, int pinOrder, CancellationToken ct = default)
740 {
741 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
742 if (config is null) return;
743 config.ShowOnHome = showOnHome;
744 config.PinOrder = pinOrder;
745 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
746 await LoadAsync(ct).ConfigureAwait(false);
747 StatusMessage = $"✅ '{slug}' 노출 설정 저장됨";
748 }
749
782 public async Task DeleteTenantAsync(string slug, CancellationToken ct = default)
783 {
784 try
785 {
786 await _tenants.DeleteAsync(slug, ct).ConfigureAwait(false);
787 await LoadAsync(ct).ConfigureAwait(false);
788 StatusMessage = $"🗑 '{slug}' 삭제 완료.";
789 }
790 catch (Exception ex)
791 {
792 StatusMessage = $"삭제 오류: {ex.Message}";
793 }
794 }
795
836 public async Task SetMaxVideoSizeAsync(string slug, int? maxVideoSizeMb, CancellationToken ct = default)
837 {
838 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
839 if (config is null) return;
840 config.MediaPolicyOverride ??= new MediaPolicyOverride();
841 config.MediaPolicyOverride.VideoMaxFileSizeMb = NormalizeNullableNonNegative(maxVideoSizeMb);
842 config.MaxVideoSizeMb = null;
843 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
844 await LoadAsync(ct).ConfigureAwait(false);
845 StatusMessage = $"✅ '{slug}' 동영상 용량 제한 저장됨";
846 }
847
888 public async Task SetMaxVideoCountAsync(string slug, int? maxVideoCount, CancellationToken ct = default)
889 {
890 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
891 if (config is null) return;
892 config.MediaPolicyOverride ??= new MediaPolicyOverride();
893 config.MediaPolicyOverride.VideoMaxCount = NormalizeNullableNonNegative(maxVideoCount);
894 config.MaxVideoCount = null;
895 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
896 await LoadAsync(ct).ConfigureAwait(false);
897 StatusMessage = $"✅ '{slug}' 동영상 개수 제한 저장됨";
898 }
899
932 public async Task SaveMediaOverrideAsync(TenantConfig tenant, CancellationToken ct = default)
933 {
934 var config = await _tenants.GetAsync(tenant.Slug, ct).ConfigureAwait(false);
935 if (config is null) return;
936
937 config.MediaPolicyOverride = NormalizeOverride(tenant.MediaPolicyOverride);
938 config.MaxVideoSizeMb = null;
939 config.MaxVideoCount = null;
940
941 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
942 await LoadAsync(ct).ConfigureAwait(false);
943 StatusMessage = $"✅ '{tenant.Slug}' 미디어 정책 override 저장됨";
944 }
945
978 public async Task ResetMediaOverrideAsync(string slug, CancellationToken ct = default)
979 {
980 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
981 if (config is null) return;
982
983 config.MediaPolicyOverride = null;
984 config.MaxVideoSizeMb = null;
985 config.MaxVideoCount = null;
986
987 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
988 await LoadAsync(ct).ConfigureAwait(false);
989 StatusMessage = $"✅ '{slug}' 계정별 미디어 override 초기화됨";
990 }
991
1024 public async Task QueueImageMigrationAsync(string slug, CancellationToken ct = default)
1025 {
1026 await _mediaMigration.QueueTenantAsync(slug, ct).ConfigureAwait(false);
1027 await LoadAsync(ct).ConfigureAwait(false);
1028 StatusMessage = $"✅ '{slug}' 이미지 최적화 작업이 대기열에 등록되었습니다.";
1029 }
1030
1063 public async Task RetryImageMigrationAsync(string slug, CancellationToken ct = default)
1064 {
1065 await _mediaMigration.RetryTenantAsync(slug, ct).ConfigureAwait(false);
1066 await LoadAsync(ct).ConfigureAwait(false);
1067 StatusMessage = $"✅ '{slug}' 이미지 최적화 실패 항목을 재시도합니다.";
1068 }
1069
1110 public async Task SetUnlockedLayoutsAsync(string slug, IEnumerable<string> unlockedLayoutModes, CancellationToken ct = default)
1111 {
1112 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
1113 if (config is null) return;
1114
1115 config.UnlockedLayoutModes = NormalizeUnlockedLayoutKeys(unlockedLayoutModes);
1116 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
1117 await LoadAsync(ct).ConfigureAwait(false);
1118 StatusMessage = $"✅ '{slug}' 레이아웃 권한 저장됨";
1119 }
1120
1161 public async Task SetUnlockedThemesAsync(string slug, IEnumerable<string> unlockedThemeKeys, CancellationToken ct = default)
1162 {
1163 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
1164 if (config is null) return;
1165
1166 config.UnlockedThemeKeys = NormalizeUnlockedThemeKeys(unlockedThemeKeys);
1167 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
1168 await LoadAsync(ct).ConfigureAwait(false);
1169 StatusMessage = $"✅ '{slug}' 테마 권한 저장됨";
1170 }
1171
1196 private static List<string> NormalizeUnlockedLayoutKeys(IEnumerable<string> unlockedLayoutModes) =>
1197 unlockedLayoutModes
1198 .Select(WeddingLayoutCatalog.FromLegacyKey)
1199 .Select(mode => WeddingLayoutCatalog.Instance.Find(mode))
1200 .Where(option => option is { Tier: WeddingLayoutTier.Premium })
1201 .Select(option => option!.Key)
1202 .Distinct(StringComparer.OrdinalIgnoreCase)
1203 .OrderBy(x => x)
1204 .ToList();
1205
1230 private static List<string> NormalizeUnlockedThemeKeys(IEnumerable<string> unlockedThemeKeys) =>
1231 unlockedThemeKeys
1232 .Select(WeddingThemeCatalog.NormalizeKey)
1233 .Select(key => WeddingThemeCatalog.Instance.Find(key))
1234 .Where(option => option is { Tier: WeddingThemeTier.Premium })
1235 .Select(option => option!.Key)
1236 .Distinct(StringComparer.OrdinalIgnoreCase)
1237 .OrderBy(x => x)
1238 .ToList();
1239
1264 private static int? NormalizeNullableNonNegative(int? value) =>
1265 value.HasValue ? Math.Max(0, value.Value) : null;
1266
1292 {
1293 if (value is null || value.IsEmpty)
1294 {
1295 return null;
1296 }
1297
1298 value.ImageMaxCount = NormalizeNullableNonNegative(value.ImageMaxCount);
1299 value.ImageOptimizedMaxStorageMb = NormalizeNullableNonNegative(value.ImageOptimizedMaxStorageMb);
1300 value.ImageOriginalMaxStorageMb = NormalizeNullableNonNegative(value.ImageOriginalMaxStorageMb);
1301 value.ImageMaxLongSidePx = NormalizeNullableNonNegative(value.ImageMaxLongSidePx);
1302 value.ImageQuality = value.ImageQuality.HasValue ? Math.Clamp(value.ImageQuality.Value, 1, 100) : null;
1303 value.VideoMaxFileSizeMb = NormalizeNullableNonNegative(value.VideoMaxFileSizeMb);
1304 value.VideoMaxCount = NormalizeNullableNonNegative(value.VideoMaxCount);
1305 value.VideoMaxStorageMb = NormalizeNullableNonNegative(value.VideoMaxStorageMb);
1306 value.ImageOutputFormat = string.IsNullOrWhiteSpace(value.ImageOutputFormat) ? null : value.ImageOutputFormat.Trim();
1307 return value.IsEmpty ? null : value;
1308 }
1309}
1310
static MediaPolicySettings CreateDefault()
MediaPolicyOverride? MediaPolicyOverride
static void Normalize(TenantConfig config)
async Task QueueImageMigrationAsync(string slug, CancellationToken ct=default)
async Task DeleteTenantAsync(string slug, CancellationToken ct=default)
async Task SetMaxVideoSizeAsync(string slug, int? maxVideoSizeMb, CancellationToken ct=default)
async Task SetUnlockedLayoutsAsync(string slug, IEnumerable< string > unlockedLayoutModes, CancellationToken ct=default)
readonly IMediaMigrationService _mediaMigration
static List< string > NormalizeUnlockedThemeKeys(IEnumerable< string > unlockedThemeKeys)
async Task SaveGlobalSettingsAsync(CancellationToken ct=default)
async Task LoadGlobalSettingsAsync(CancellationToken ct=default)
async Task SaveMediaOverrideAsync(TenantConfig tenant, CancellationToken ct=default)
SuperAdminViewModel(ITenantStore tenants, WeddingOptions opts, IGlobalSettingsStore globalSettings, ThankYouUserContext userContext, IMediaUsageQueryService mediaUsage, IMediaMigrationService mediaMigration, ISuperAdminSessionTokenService superAdminTokens)
async Task SetUnlockedThemesAsync(string slug, IEnumerable< string > unlockedThemeKeys, CancellationToken ct=default)
static List< string > NormalizeUnlockedLayoutKeys(IEnumerable< string > unlockedLayoutModes)
IReadOnlyDictionary< string, MediaMigrationTenantStatus > MigrationStatuses
async Task SetMaxVideoCountAsync(string slug, int? maxVideoCount, CancellationToken ct=default)
MediaMigrationState ResolveMigrationState(TenantConfig tenant)
async Task ResetMediaOverrideAsync(string slug, CancellationToken ct=default)
async Task< bool > CreateTenantAsync(CancellationToken ct=default)
static ? MediaPolicyOverride NormalizeOverride(MediaPolicyOverride? value)
IReadOnlyDictionary< string, TenantMediaUsageSummary > MediaUsage
async Task ReloadTenantsAsync(CancellationToken ct)
async Task RefreshMediaUsageAsync(CancellationToken ct=default)
async Task LoadAsync(CancellationToken ct=default)
async Task RetryImageMigrationAsync(string slug, CancellationToken ct=default)
IReadOnlyDictionary< string, TenantMediaUsageSummary > BuildFastMediaUsage()
readonly ISuperAdminSessionTokenService _superAdminTokens
async Task SetDisplayAsync(string slug, bool showOnHome, int pinOrder, CancellationToken ct=default)