Families.Web 1.0.0.0
사진, 동영상, 글, 댓글과 반응을 가족끼리만 공유하는 비공개 앨범·타임라인 서비스입니다.
로딩중...
검색중...
일치하는것 없음
FamilyPostViewModel.cs
이 파일의 문서화 페이지로 가기
1using Markdig;
4
6
15public sealed class FamilyPostViewModel
16{
25 private readonly IFamilyTenantStore _tenants;
34 private readonly IPostStore _posts;
43 private readonly IReactionStore _reactions;
52 private readonly IMediaService _media;
53
62 private static readonly MarkdownPipeline _mdPipeline =
63 new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
64
106 IReactionStore reactions, IMediaService media)
107 {
108 _tenants = tenants;
109 _posts = posts;
110 _reactions = reactions;
111 _media = media;
112 }
113
122 public FamilyConfig? Config { get; private set; }
131 public PostEntry? Post { get; private set; }
140 public ReactionSummary Reactions { get; private set; } = new();
149 public bool IsLoaded { get; private set; }
158 public bool NotFound { get; private set; }
159
168 public string ContentHtml => string.IsNullOrWhiteSpace(Post?.Content)
169 ? ""
170 : Markdown.ToHtml(Post.Content, _mdPipeline);
171
180 public bool AllowReactions => Config?.AllowReactions ?? true;
189 public bool AllowComments => Config?.AllowComments ?? true;
198 public string ThemeName => Config?.ThemeName ?? "warm";
207 public string FamilyName => Config?.FamilyName ?? "";
208
233 public string GetMediaUrl(string fileName) =>
234 IsExternalUrl(fileName) ? fileName
235 : Post is null ? "" : _media.GetMediaUrl(Config?.Slug ?? "", Post.Id, fileName);
236
261 public string GetThumbUrl(string fileName) =>
262 IsExternalUrl(fileName) ? fileName
263 : Post is null ? "" : _media.GetThumbUrl(Config?.Slug ?? "", Post.Id, fileName);
264
289 public string GetVideoUrl(string fileName) =>
290 IsExternalUrl(fileName) ? fileName : GetMediaUrl(fileName);
291
316 public static bool IsExternalUrl(string s) =>
317 s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
318 s.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
319
344 public static bool IsYouTube(string url) =>
345 url.Contains("youtube.com", StringComparison.OrdinalIgnoreCase) ||
346 url.Contains("youtu.be/", StringComparison.OrdinalIgnoreCase);
347
372 public static string GetYouTubeEmbedUrl(string url)
373 {
374 url = url.Trim();
375
376 // 이미 embed URL이면 그대로
377 if (url.Contains("/embed/", StringComparison.OrdinalIgnoreCase))
378 return url.Split('?')[0]; // 불필요한 쿼리 제거
379
380 // youtu.be/ID 단축 URL
381 if (url.Contains("youtu.be/", StringComparison.OrdinalIgnoreCase))
382 {
383 var after = url.Split(new[] { "youtu.be/" }, StringSplitOptions.None)[1];
384 var id = after.Split('?')[0].Split('#')[0].Trim();
385 return $"https://www.youtube.com/embed/{id}";
386 }
387
388 // youtube.com/shorts/ID
389 var shortsMatch = System.Text.RegularExpressions.Regex.Match(
390 url, @"youtube\.com/shorts/([^?&#/]+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
391 if (shortsMatch.Success)
392 return $"https://www.youtube.com/embed/{shortsMatch.Groups[1].Value}";
393
394 // youtube.com/watch?v=ID (또는 &v=ID)
395 var watchMatch = System.Text.RegularExpressions.Regex.Match(url, @"[?&]v=([^?&#]+)");
396 if (watchMatch.Success)
397 return $"https://www.youtube.com/embed/{watchMatch.Groups[1].Value}";
398
399 // 그래도 못 찾으면 원본 반환 (브라우저가 처리)
400 return url;
401 }
402
403 // ── 댓글 작성 ──────────────────────────────────────────
412 public string CommentAuthor { get; set; } = "";
421 public string CommentBody { get; set; } = "";
430 public string CommentStatus { get; private set; } = "";
439 public bool IsSavingComment { get; private set; }
440
481 public async Task LoadAsync(string slug, string postId, CancellationToken ct = default)
482 {
483 Config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
484 if (Config is null) { NotFound = true; IsLoaded = true; return; }
485
486 Post = await _posts.GetAsync(slug, postId, ct).ConfigureAwait(false);
487 if (Post is null) { NotFound = true; IsLoaded = true; return; }
488
489 Reactions = await _reactions.GetAsync(slug, postId, ct).ConfigureAwait(false);
490 IsLoaded = true;
491 }
492
533 public async Task AddReactionAsync(string slug, string emoji, CancellationToken ct = default)
534 {
535 if (Post is null || !AllowReactions) return;
536 await _reactions.AddReactionAsync(slug, Post.Id, emoji, ct).ConfigureAwait(false);
537 Reactions = await _reactions.GetAsync(slug, Post.Id, ct).ConfigureAwait(false);
538 }
539
572 public async Task AddCommentAsync(string slug, CancellationToken ct = default)
573 {
574 if (Post is null || !AllowComments || IsSavingComment) return;
575 if (string.IsNullOrWhiteSpace(CommentAuthor)) { CommentStatus = "이름을 입력해주세요."; return; }
576 if (string.IsNullOrWhiteSpace(CommentBody)) { CommentStatus = "댓글 내용을 입력해주세요."; return; }
577
578 IsSavingComment = true;
579 try
580 {
581 var entry = new CommentEntry
582 {
583 PostId = Post.Id,
584 AuthorName = CommentAuthor.Trim(),
585 Body = CommentBody.Trim(),
586 CreatedAt = DateTime.Now
587 };
588 await _reactions.AddCommentAsync(slug, Post.Id, entry, ct).ConfigureAwait(false);
589 Reactions = await _reactions.GetAsync(slug, Post.Id, ct).ConfigureAwait(false);
590 CommentAuthor = "";
591 CommentBody = "";
592 CommentStatus = "댓글이 등록되었습니다 💬";
593 }
594 catch { CommentStatus = "등록 중 오류가 발생했습니다."; }
595 finally { IsSavingComment = false; }
596 }
597
638 public async Task DeleteCommentAsync(string slug, string commentId, CancellationToken ct = default)
639 {
640 if (Post is null) return;
641 await _reactions.DeleteCommentAsync(slug, Post.Id, commentId, ct).ConfigureAwait(false);
642 Reactions = await _reactions.GetAsync(slug, Post.Id, ct).ConfigureAwait(false);
643 }
644}
FamilyPostViewModel(IFamilyTenantStore tenants, IPostStore posts, IReactionStore reactions, IMediaService media)
async Task AddCommentAsync(string slug, CancellationToken ct=default)
async Task DeleteCommentAsync(string slug, string commentId, CancellationToken ct=default)
async Task AddReactionAsync(string slug, string emoji, CancellationToken ct=default)
async Task LoadAsync(string slug, string postId, CancellationToken ct=default)
static readonly MarkdownPipeline _mdPipeline