WeddingPlatform.Web 1.0.0.0
지도, 갤러리, 방명록, 계좌 안내와 배경음악을 링크 하나에 담는 무료 모바일 청첩장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
WeddingSuperAdminViewModel.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using Dreamine.Identity;
5using Wedding.Common;
6
8
17public sealed class WeddingSuperAdminViewModel
18{
27 private readonly ITenantStore _tenants;
36 private readonly WeddingOptions _opts;
82
148 ITenantStore tenants,
149 WeddingOptions opts,
150 IGlobalSettingsStore globalSettings,
151 WeddingUserContext 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 == "admin")
646 { StatusMessage = "'admin'은 슬러그로 사용할 수 없습니다."; 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 Mode = WeddingSiteMode.Invite,
659 IsPublished = true
660 };
661
662 if (user.IsAuthenticated)
663 {
664 config.OwnerUserId = user.Id;
665 config.OwnerProvider = user.Provider;
666 config.OwnerEmail = user.Email;
667 config.OwnerDisplayName = user.DisplayName;
668 config.OwnerLinkedAt = DateTime.Now;
669 config.AdminUsers.Add(new WeddingAdminUser
670 {
671 UserId = user.Id,
672 Provider = user.Provider,
673 Email = user.Email,
674 DisplayName = user.DisplayName,
675 Role = "Owner",
676 AddedAt = DateTime.Now
677 });
678 }
679
680 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
681 await LoadAsync(ct).ConfigureAwait(false);
682
683 NewSlug = "";
684 NewCoupleName = "";
685 NewPassword = "";
686 NewWeddingDate = DateTime.Today.AddMonths(3);
687
688 StatusMessage = $"✅ '{slug}' 청첩장이 생성되었습니다.";
689 return true;
690 }
691
740 public async Task SetDisplayAsync(string slug, bool showOnHome, int pinOrder, CancellationToken ct = default)
741 {
742 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
743 if (config is null) return;
744 config.ShowOnHome = showOnHome;
745 config.PinOrder = pinOrder;
746 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
747 await LoadAsync(ct).ConfigureAwait(false);
748 StatusMessage = $"✅ '{slug}' 노출 설정 저장됨";
749 }
750
783 public async Task DeleteTenantAsync(string slug, CancellationToken ct = default)
784 {
785 try
786 {
787 await _tenants.DeleteAsync(slug, ct).ConfigureAwait(false);
788 await LoadAsync(ct).ConfigureAwait(false);
789 StatusMessage = $"🗑 '{slug}' 삭제 완료.";
790 }
791 catch (Exception ex)
792 {
793 StatusMessage = $"삭제 오류: {ex.Message}";
794 }
795 }
796
837 public async Task SetMaxVideoSizeAsync(string slug, int? maxVideoSizeMb, CancellationToken ct = default)
838 {
839 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
840 if (config is null) return;
841 config.MediaPolicyOverride ??= new MediaPolicyOverride();
842 config.MediaPolicyOverride.VideoMaxFileSizeMb = NormalizeNullableNonNegative(maxVideoSizeMb);
843 config.MaxVideoSizeMb = null;
844 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
845 await LoadAsync(ct).ConfigureAwait(false);
846 StatusMessage = $"✅ '{slug}' 동영상 용량 제한 저장됨";
847 }
848
889 public async Task SetMaxVideoCountAsync(string slug, int? maxVideoCount, CancellationToken ct = default)
890 {
891 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
892 if (config is null) return;
893 config.MediaPolicyOverride ??= new MediaPolicyOverride();
894 config.MediaPolicyOverride.VideoMaxCount = NormalizeNullableNonNegative(maxVideoCount);
895 config.MaxVideoCount = null;
896 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
897 await LoadAsync(ct).ConfigureAwait(false);
898 StatusMessage = $"✅ '{slug}' 동영상 개수 제한 저장됨";
899 }
900
933 public async Task SaveMediaOverrideAsync(TenantConfig tenant, CancellationToken ct = default)
934 {
935 var config = await _tenants.GetAsync(tenant.Slug, ct).ConfigureAwait(false);
936 if (config is null) return;
937
938 config.MediaPolicyOverride = NormalizeOverride(tenant.MediaPolicyOverride);
939 config.MaxVideoSizeMb = null;
940 config.MaxVideoCount = null;
941
942 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
943 await LoadAsync(ct).ConfigureAwait(false);
944 StatusMessage = $"✅ '{tenant.Slug}' 미디어 정책 override 저장됨";
945 }
946
979 public async Task ResetMediaOverrideAsync(string slug, CancellationToken ct = default)
980 {
981 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
982 if (config is null) return;
983
984 config.MediaPolicyOverride = null;
985 config.MaxVideoSizeMb = null;
986 config.MaxVideoCount = null;
987
988 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
989 await LoadAsync(ct).ConfigureAwait(false);
990 StatusMessage = $"✅ '{slug}' 계정별 미디어 override 초기화됨";
991 }
992
1025 public async Task QueueImageMigrationAsync(string slug, CancellationToken ct = default)
1026 {
1027 await _mediaMigration.QueueTenantAsync(slug, ct).ConfigureAwait(false);
1028 await LoadAsync(ct).ConfigureAwait(false);
1029 StatusMessage = $"✅ '{slug}' 이미지 최적화 작업이 대기열에 등록되었습니다.";
1030 }
1031
1064 public async Task RetryImageMigrationAsync(string slug, CancellationToken ct = default)
1065 {
1066 await _mediaMigration.RetryTenantAsync(slug, ct).ConfigureAwait(false);
1067 await LoadAsync(ct).ConfigureAwait(false);
1068 StatusMessage = $"✅ '{slug}' 이미지 최적화 실패 항목을 재시도합니다.";
1069 }
1070
1111 public async Task SetUnlockedLayoutsAsync(string slug, IEnumerable<string> unlockedLayoutModes, CancellationToken ct = default)
1112 {
1113 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
1114 if (config is null) return;
1115
1116 config.UnlockedLayoutModes = NormalizeUnlockedLayoutKeys(unlockedLayoutModes);
1117 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
1118 await LoadAsync(ct).ConfigureAwait(false);
1119 StatusMessage = $"✅ '{slug}' 레이아웃 권한 저장됨";
1120 }
1121
1162 public async Task SetUnlockedThemesAsync(string slug, IEnumerable<string> unlockedThemeKeys, CancellationToken ct = default)
1163 {
1164 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
1165 if (config is null) return;
1166
1167 config.UnlockedThemeKeys = NormalizeUnlockedThemeKeys(unlockedThemeKeys);
1168 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
1169 await LoadAsync(ct).ConfigureAwait(false);
1170 StatusMessage = $"✅ '{slug}' 테마 권한 저장됨";
1171 }
1172
1197 private static List<string> NormalizeUnlockedLayoutKeys(IEnumerable<string> unlockedLayoutModes) =>
1198 unlockedLayoutModes
1199 .Select(WeddingLayoutCatalog.FromLegacyKey)
1200 .Select(mode => WeddingLayoutCatalog.Instance.Find(mode))
1201 .Where(option => option is { Tier: WeddingLayoutTier.Premium })
1202 .Select(option => option!.Key)
1203 .Distinct(StringComparer.OrdinalIgnoreCase)
1204 .OrderBy(x => x)
1205 .ToList();
1206
1231 private static List<string> NormalizeUnlockedThemeKeys(IEnumerable<string> unlockedThemeKeys) =>
1232 unlockedThemeKeys
1233 .Select(WeddingThemeCatalog.NormalizeKey)
1234 .Select(key => WeddingThemeCatalog.Instance.Find(key))
1235 .Where(option => option is { Tier: WeddingThemeTier.Premium })
1236 .Select(option => option!.Key)
1237 .Distinct(StringComparer.OrdinalIgnoreCase)
1238 .OrderBy(x => x)
1239 .ToList();
1240
1265 private static int? NormalizeNullableNonNegative(int? value) =>
1266 value.HasValue ? Math.Max(0, value.Value) : null;
1267
1293 {
1294 if (value is null || value.IsEmpty)
1295 {
1296 return null;
1297 }
1298
1299 value.ImageMaxCount = NormalizeNullableNonNegative(value.ImageMaxCount);
1300 value.ImageOptimizedMaxStorageMb = NormalizeNullableNonNegative(value.ImageOptimizedMaxStorageMb);
1301 value.ImageOriginalMaxStorageMb = NormalizeNullableNonNegative(value.ImageOriginalMaxStorageMb);
1302 value.ImageMaxLongSidePx = NormalizeNullableNonNegative(value.ImageMaxLongSidePx);
1303 value.ImageQuality = value.ImageQuality.HasValue ? Math.Clamp(value.ImageQuality.Value, 1, 100) : null;
1304 value.VideoMaxFileSizeMb = NormalizeNullableNonNegative(value.VideoMaxFileSizeMb);
1305 value.VideoMaxCount = NormalizeNullableNonNegative(value.VideoMaxCount);
1306 value.VideoMaxStorageMb = NormalizeNullableNonNegative(value.VideoMaxStorageMb);
1307 value.ImageOutputFormat = string.IsNullOrWhiteSpace(value.ImageOutputFormat) ? null : value.ImageOutputFormat.Trim();
1308 return value.IsEmpty ? null : value;
1309 }
1310}
static void Normalize(TenantConfig config)
static MediaPolicySettings CreateDefault()
MediaPolicyOverride? MediaPolicyOverride
async Task SetMaxVideoCountAsync(string slug, int? maxVideoCount, CancellationToken ct=default)
static ? MediaPolicyOverride NormalizeOverride(MediaPolicyOverride? value)
async Task SaveGlobalSettingsAsync(CancellationToken ct=default)
async Task RefreshMediaUsageAsync(CancellationToken ct=default)
IReadOnlyDictionary< string, MediaMigrationTenantStatus > MigrationStatuses
async Task SetMaxVideoSizeAsync(string slug, int? maxVideoSizeMb, CancellationToken ct=default)
async Task< bool > CreateTenantAsync(CancellationToken ct=default)
async Task LoadGlobalSettingsAsync(CancellationToken ct=default)
async Task SaveMediaOverrideAsync(TenantConfig tenant, CancellationToken ct=default)
WeddingSuperAdminViewModel(ITenantStore tenants, WeddingOptions opts, IGlobalSettingsStore globalSettings, WeddingUserContext userContext, IMediaUsageQueryService mediaUsage, IMediaMigrationService mediaMigration, ISuperAdminSessionTokenService superAdminTokens)
async Task QueueImageMigrationAsync(string slug, CancellationToken ct=default)
static List< string > NormalizeUnlockedLayoutKeys(IEnumerable< string > unlockedLayoutModes)
async Task ResetMediaOverrideAsync(string slug, CancellationToken ct=default)
async Task SetUnlockedLayoutsAsync(string slug, IEnumerable< string > unlockedLayoutModes, CancellationToken ct=default)
IReadOnlyDictionary< string, TenantMediaUsageSummary > BuildFastMediaUsage()
IReadOnlyDictionary< string, TenantMediaUsageSummary > MediaUsage
static List< string > NormalizeUnlockedThemeKeys(IEnumerable< string > unlockedThemeKeys)
async Task RetryImageMigrationAsync(string slug, CancellationToken ct=default)
async Task SetDisplayAsync(string slug, bool showOnHome, int pinOrder, CancellationToken ct=default)
async Task DeleteTenantAsync(string slug, CancellationToken ct=default)
async Task SetUnlockedThemesAsync(string slug, IEnumerable< string > unlockedThemeKeys, CancellationToken ct=default)
MediaMigrationState ResolveMigrationState(TenantConfig tenant)