Families.Web 1.0.0.0
사진, 동영상, 글, 댓글과 반응을 가족끼리만 공유하는 비공개 앨범·타임라인 서비스입니다.
로딩중...
검색중...
일치하는것 없음
FamilyAdminViewModel.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using Dreamine.Identity;
5using Microsoft.AspNetCore.Components.Forms;
6
8
17public sealed class FamilyAdminViewModel
18{
27 private readonly IFamilyTenantStore _tenants;
36 private readonly IPostStore _posts;
45 private readonly IAlbumStore _albums;
54 private readonly IMediaService _media;
64
114 IAlbumStore albums, IMediaService media, FamilyUserContext userContext)
115 {
116 _tenants = tenants;
117 _posts = posts;
118 _albums = albums;
119 _media = media;
120 _userContext = userContext;
121 }
122
131 public FamilyConfig? Config { get; private set; }
140 public IReadOnlyList<PostEntry> Posts { get; private set; } = [];
149 public IReadOnlyList<AlbumInfo> Albums { get; private set; } = [];
158 public bool IsLoaded { get; private set; }
167 public bool IsAuthenticated { get; private set; }
176 public bool IsSignedIn { get; private set; }
185 public bool IsLinkedToCurrentUser { get; private set; }
194 public bool IsOwner { get; private set; }
203 public string StatusMessage { get; private set; } = "";
212 public string CurrentUserLabel { get; private set; } = "";
221 public bool IsUploading { get; private set; }
230 public string LoginPassword { get; set; } = "";
239 public IReadOnlyList<FamilyAdminUser> EffectiveAdminUsers =>
240 Config is null ? [] : BuildEffectiveAdminUsers(Config);
241
242 // ── 포스트 편집 ─────────────────────────────────────────
251 public PostEntry? EditingPost { get; private set; }
260 public List<IBrowserFile> PendingFiles { get; private set; } = new();
261
262 // ── 앨범 편집 ───────────────────────────────────────────
271 public string NewAlbumName { get; set; } = "";
280 public string NewAlbumDesc { get; set; } = "";
281
314 public async Task<bool> LoginAsync(string slug, CancellationToken ct = default)
315 {
316 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
317 if (config is null) { StatusMessage = "존재하지 않는 슬러그입니다."; return false; }
318
319 var user = await _userContext.GetCurrentAsync().ConfigureAwait(false);
320 await RefreshCurrentUserAsync().ConfigureAwait(false);
321
322 if (IsAdminUser(config, user))
323 {
324 IsAuthenticated = true;
326 IsOwner = IsOwnerUser(config, user);
327 StatusMessage = "";
328 return true;
329 }
330
331 var verification = DreaminePasswordHasher.VerifyPassword(LoginPassword, config.PasswordHash, out var upgradedHash);
332 IsAuthenticated = verification is not PasswordHashVerificationResult.Failed;
333 if (!IsAuthenticated)
334 {
335 StatusMessage = "비밀번호가 틀렸습니다.";
336 return false;
337 }
338
339 if (verification is PasswordHashVerificationResult.SuccessRehashNeeded && upgradedHash is not null)
340 {
341 config.PasswordHash = upgradedHash;
342 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
343 }
344
345 if (user.IsAuthenticated && string.IsNullOrWhiteSpace(config.OwnerUserId))
346 {
347 config.OwnerUserId = user.Id;
348 config.OwnerProvider = user.Provider;
349 config.OwnerEmail = user.Email;
350 config.OwnerDisplayName = user.DisplayName;
351 config.OwnerLinkedAt = DateTime.Now;
352 EnsureAdminUser(config, user, "Owner");
353 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
355 IsOwner = true;
356 StatusMessage = "CodeMaru/Dreamine 계정에 연결되었습니다. 다음부터는 공용 로그인으로 관리할 수 있습니다.";
357 }
358 else if (user.IsAuthenticated)
359 {
360 EnsureAdminUser(config, user, "Admin");
361 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
363 IsOwner = IsOwnerUser(config, user);
364 StatusMessage = "현재 CodeMaru/Dreamine 계정이 이 가족 앨범의 관리자로 추가되었습니다.";
365 }
366 else if (!user.IsAuthenticated && string.IsNullOrWhiteSpace(config.OwnerUserId))
367 {
368 StatusMessage = "로그인은 성공했습니다. 공용 계정에 연결하려면 먼저 CodeMaru/Dreamine 로그인을 해주세요.";
369 }
370 else
371 {
372 StatusMessage = "";
373 }
374
375 return IsAuthenticated;
376 }
377
410 public async Task InitializeAsync(string slug, CancellationToken ct = default)
411 {
412 StatusMessage = "";
413 await RefreshCurrentUserAsync().ConfigureAwait(false);
414
415 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
416 if (config is null)
417 {
418 return;
419 }
420
421 var user = await _userContext.GetCurrentAsync().ConfigureAwait(false);
422 IsLinkedToCurrentUser = IsAdminUser(config, user);
423 IsOwner = IsOwnerUser(config, user);
424
426 {
427 IsAuthenticated = true;
428 await LoadAsync(slug, ct).ConfigureAwait(false);
429 }
430 }
431
464 public async Task LoadAsync(string slug, CancellationToken ct = default)
465 {
466 Config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false)
467 ?? new FamilyConfig { Slug = slug };
468 Posts = await _posts.GetAllAsync(slug, ct).ConfigureAwait(false);
469 Albums = await _albums.GetAllAsync(slug, ct).ConfigureAwait(false);
470 IsLoaded = true;
471 }
472
505 public async Task RemoveAdminAsync(string userId, CancellationToken ct = default)
506 {
507 if (Config is null || string.IsNullOrWhiteSpace(userId))
508 {
509 return;
510 }
511
512 var user = await _userContext.GetCurrentAsync().ConfigureAwait(false);
513 if (!IsOwnerUser(Config, user))
514 {
515 StatusMessage = "대표 관리자만 관리자를 삭제할 수 있습니다.";
516 return;
517 }
518
519 if (string.Equals(Config.OwnerUserId, userId, StringComparison.Ordinal))
520 {
521 StatusMessage = "대표 관리자는 삭제할 수 없습니다.";
522 return;
523 }
524
525 var removed = Config.AdminUsers.RemoveAll(x =>
526 string.Equals(x.UserId, userId, StringComparison.Ordinal)) > 0;
527 if (!removed)
528 {
529 StatusMessage = "삭제할 관리자를 찾을 수 없습니다.";
530 return;
531 }
532
533 await _tenants.SaveAsync(Config, ct).ConfigureAwait(false);
534 StatusMessage = "관리자가 삭제되었습니다.";
535 }
536
553 private async Task RefreshCurrentUserAsync()
554 {
555 var user = await _userContext.GetCurrentAsync().ConfigureAwait(false);
556 IsSignedIn = user.IsAuthenticated;
557 CurrentUserLabel = user.IsAuthenticated
558 ? string.IsNullOrWhiteSpace(user.DisplayName)
559 ? user.Provider
560 : $"{user.DisplayName} ({user.Provider})"
561 : "";
562 }
563
596 private static bool IsOwnerUser(FamilyConfig config, FamilyCurrentUser user) =>
597 user.IsAuthenticated &&
598 string.Equals(config.OwnerUserId, user.Id, StringComparison.Ordinal);
599
632 private static bool IsAdminUser(FamilyConfig config, FamilyCurrentUser user) =>
633 user.IsAuthenticated &&
634 (IsOwnerUser(config, user) ||
635 config.AdminUsers.Any(x => string.Equals(x.UserId, user.Id, StringComparison.Ordinal)));
636
669 private static void EnsureAdminUser(FamilyConfig config, FamilyCurrentUser user, string role)
670 {
671 var existing = config.AdminUsers.FirstOrDefault(x =>
672 string.Equals(x.UserId, user.Id, StringComparison.Ordinal));
673 if (existing is null)
674 {
675 config.AdminUsers.Add(new FamilyAdminUser
676 {
677 UserId = user.Id,
678 Provider = user.Provider,
679 Email = user.Email,
680 DisplayName = user.DisplayName,
681 Role = role,
682 AddedAt = DateTime.Now
683 });
684 return;
685 }
686
687 existing.Provider = user.Provider;
688 existing.Email = user.Email;
689 existing.DisplayName = user.DisplayName;
690 if (string.Equals(role, "Owner", StringComparison.OrdinalIgnoreCase))
691 {
692 existing.Role = "Owner";
693 }
694 }
695
720 private static IReadOnlyList<FamilyAdminUser> BuildEffectiveAdminUsers(FamilyConfig config)
721 {
722 var result = new List<FamilyAdminUser>();
723 if (!string.IsNullOrWhiteSpace(config.OwnerUserId))
724 {
725 result.Add(new FamilyAdminUser
726 {
727 UserId = config.OwnerUserId,
728 Provider = config.OwnerProvider,
729 Email = config.OwnerEmail,
730 DisplayName = config.OwnerDisplayName,
731 Role = "Owner",
732 AddedAt = config.OwnerLinkedAt ?? DateTime.MinValue
733 });
734 }
735
736 foreach (var admin in config.AdminUsers)
737 {
738 var existing = result.FirstOrDefault(x =>
739 string.Equals(x.UserId, admin.UserId, StringComparison.Ordinal));
740 if (existing is null)
741 {
742 result.Add(admin);
743 }
744 else if (string.Equals(existing.Role, "Owner", StringComparison.OrdinalIgnoreCase))
745 {
746 existing.Provider = string.IsNullOrWhiteSpace(existing.Provider) ? admin.Provider : existing.Provider;
747 existing.Email = string.IsNullOrWhiteSpace(existing.Email) ? admin.Email : existing.Email;
748 existing.DisplayName = string.IsNullOrWhiteSpace(existing.DisplayName) ? admin.DisplayName : existing.DisplayName;
749 }
750 }
751
752 return result
753 .OrderBy(x => string.Equals(x.Role, "Owner", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
754 .ThenBy(x => x.AddedAt)
755 .ToList();
756 }
757
782 public async Task SaveConfigAsync(CancellationToken ct = default)
783 {
784 if (Config is null) return;
785 try
786 {
787 Config.PasswordHash = DreaminePasswordHasher.HashPlainTextForStorage(Config.PasswordHash);
788 await _tenants.SaveAsync(Config, ct).ConfigureAwait(false);
789 StatusMessage = "설정이 저장되었습니다.";
790 }
791 catch (Exception ex) { StatusMessage = $"저장 오류: {ex.Message}"; }
792 }
793
802 public void NewPost()
803 {
804 EditingPost = new PostEntry { PostedAt = DateTime.Now };
805 PendingFiles.Clear();
806 StatusMessage = "";
807 }
808
825 public void EditPost(PostEntry post)
826 {
827 EditingPost = post;
828 PendingFiles.Clear();
829 StatusMessage = "";
830 }
831
840 public void CancelEdit()
841 {
842 EditingPost = null;
843 PendingFiles.Clear();
844 }
845
846 // 파일 선택 즉시 업로드 — 편집 상태 유지, 포스트 메타는 저장하지 않음
879 public async Task UploadPendingFilesAsync(string slug, CancellationToken ct = default)
880 {
881 if (EditingPost is null || PendingFiles.Count == 0) return;
882 IsUploading = true;
883 try
884 {
885 foreach (var file in PendingFiles)
886 {
887 var fn = await _media.UploadPostMediaAsync(slug, EditingPost.Id, file, ct).ConfigureAwait(false);
888 var ext = Path.GetExtension(fn).ToLowerInvariant();
889 if (ext is ".mp4" or ".webm" or ".mov" or ".m4v")
890 EditingPost.VideoFileNames.Add(fn);
891 else
892 EditingPost.PhotoFileNames.Add(fn);
893 }
894 PendingFiles.Clear();
895 StatusMessage = "파일이 추가되었습니다.";
896 }
897 catch (Exception ex) { StatusMessage = $"업로드 오류: {ex.Message}"; }
898 finally { IsUploading = false; }
899 }
900
933 public async Task SavePostAsync(string slug, CancellationToken ct = default)
934 {
935 if (EditingPost is null) return;
936 // 혹시 아직 대기 중인 파일이 있으면 먼저 업로드
937 if (PendingFiles.Count > 0)
938 await UploadPendingFilesAsync(slug, ct).ConfigureAwait(false);
939 IsUploading = true;
940 try
941 {
942 await _posts.SaveAsync(slug, EditingPost, ct).ConfigureAwait(false);
943 Posts = await _posts.GetAllAsync(slug, ct).ConfigureAwait(false);
944 EditingPost = null;
945 StatusMessage = "포스트가 저장되었습니다. ✅";
946 }
947 catch (Exception ex) { StatusMessage = $"저장 오류: {ex.Message}"; }
948 finally { IsUploading = false; }
949 }
950
991 public async Task DeletePostAsync(string slug, string postId, CancellationToken ct = default)
992 {
993 try
994 {
995 await _posts.DeleteAsync(slug, postId, ct).ConfigureAwait(false);
996 Posts = await _posts.GetAllAsync(slug, ct).ConfigureAwait(false);
997 StatusMessage = "포스트가 삭제되었습니다.";
998 }
999 catch (Exception ex) { StatusMessage = $"삭제 오류: {ex.Message}"; }
1000 }
1001
1050 public async Task DeletePostMediaAsync(string slug, string postId, string fileName, CancellationToken ct = default)
1051 {
1052 try
1053 {
1054 await _media.DeletePostMediaAsync(slug, postId, fileName, ct).ConfigureAwait(false);
1055 if (EditingPost is not null)
1056 {
1057 EditingPost.PhotoFileNames.Remove(fileName);
1058 EditingPost.VideoFileNames.Remove(fileName);
1059 }
1060 StatusMessage = "파일이 삭제되었습니다.";
1061 }
1062 catch (Exception ex) { StatusMessage = $"삭제 오류: {ex.Message}"; }
1063 }
1064
1105 public async Task UploadCoverAsync(string slug, IBrowserFile file, CancellationToken ct = default)
1106 {
1107 IsUploading = true;
1108 try
1109 {
1110 await _media.UploadCoverAsync(slug, file, ct).ConfigureAwait(false);
1111 Config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
1112 StatusMessage = "커버 이미지가 업로드되었습니다.";
1113 }
1114 catch (Exception ex) { StatusMessage = $"업로드 오류: {ex.Message}"; }
1115 finally { IsUploading = false; }
1116 }
1117
1150 public async Task AddAlbumAsync(string slug, CancellationToken ct = default)
1151 {
1152 if (string.IsNullOrWhiteSpace(NewAlbumName)) { StatusMessage = "앨범 이름을 입력해주세요."; return; }
1153 try
1154 {
1155 var album = new AlbumInfo { Name = NewAlbumName.Trim(), Description = NewAlbumDesc.Trim() };
1156 await _albums.SaveAsync(slug, album, ct).ConfigureAwait(false);
1157 Albums = await _albums.GetAllAsync(slug, ct).ConfigureAwait(false);
1158 NewAlbumName = "";
1159 NewAlbumDesc = "";
1160 StatusMessage = "앨범이 추가되었습니다.";
1161 }
1162 catch (Exception ex) { StatusMessage = $"앨범 추가 오류: {ex.Message}"; }
1163 }
1164
1205 public async Task DeleteAlbumAsync(string slug, string albumId, CancellationToken ct = default)
1206 {
1207 try
1208 {
1209 await _albums.DeleteAsync(slug, albumId, ct).ConfigureAwait(false);
1210 Albums = await _albums.GetAllAsync(slug, ct).ConfigureAwait(false);
1211 StatusMessage = "앨범이 삭제되었습니다.";
1212 }
1213 catch (Exception ex) { StatusMessage = $"앨범 삭제 오류: {ex.Message}"; }
1214 }
1215
1248 public async Task DeleteSelfAsync(string slug, CancellationToken ct = default)
1249 {
1250 await _tenants.DeleteAsync(slug, ct).ConfigureAwait(false);
1251 }
1252
1285 public string GetCoverUrl(string slug, string fileName) => _media.GetCoverUrl(slug, fileName);
1326 public string GetMediaUrl(string slug, string postId, string fileName) => _media.GetMediaUrl(slug, postId, fileName);
1367 public string GetThumbUrl(string slug, string postId, string fileName) => _media.GetThumbUrl(slug, postId, fileName);
1368
1393 public static bool IsYouTube(string url) =>
1394 url.Contains("youtube.com", StringComparison.OrdinalIgnoreCase) ||
1395 url.Contains("youtu.be/", StringComparison.OrdinalIgnoreCase);
1396}
record FamilyCurrentUser(string Id, string Provider, string Email, string DisplayName)
List< FamilyAdminUser > AdminUsers
async Task UploadCoverAsync(string slug, IBrowserFile file, CancellationToken ct=default)
string GetCoverUrl(string slug, string fileName)
FamilyAdminViewModel(IFamilyTenantStore tenants, IPostStore posts, IAlbumStore albums, IMediaService media, FamilyUserContext userContext)
async Task DeleteSelfAsync(string slug, CancellationToken ct=default)
string GetMediaUrl(string slug, string postId, string fileName)
async Task LoadAsync(string slug, CancellationToken ct=default)
static bool IsAdminUser(FamilyConfig config, FamilyCurrentUser user)
static bool IsOwnerUser(FamilyConfig config, FamilyCurrentUser user)
IReadOnlyList< FamilyAdminUser > EffectiveAdminUsers
async Task DeletePostMediaAsync(string slug, string postId, string fileName, CancellationToken ct=default)
static IReadOnlyList< FamilyAdminUser > BuildEffectiveAdminUsers(FamilyConfig config)
async Task UploadPendingFilesAsync(string slug, CancellationToken ct=default)
async Task DeleteAlbumAsync(string slug, string albumId, CancellationToken ct=default)
async Task SavePostAsync(string slug, CancellationToken ct=default)
static void EnsureAdminUser(FamilyConfig config, FamilyCurrentUser user, string role)
async Task DeletePostAsync(string slug, string postId, CancellationToken ct=default)
async Task InitializeAsync(string slug, CancellationToken ct=default)
async Task RemoveAdminAsync(string userId, CancellationToken ct=default)
async Task AddAlbumAsync(string slug, CancellationToken ct=default)
async Task< bool > LoginAsync(string slug, CancellationToken ct=default)
string GetThumbUrl(string slug, string postId, string fileName)
async Task SaveConfigAsync(CancellationToken ct=default)