WeddingThankYou 1.0.0.0
결혼식 이후 감사 인사와 사진, 계좌 안내, 연락처를 모바일 페이지로 전달하는 감사장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
InvitationViewModel.cs
이 파일의 문서화 페이지로 가기
1using Markdig;
2using Wedding.Common;
5
7{
16 public sealed class InvitationViewModel
17 {
26 private readonly ITenantStore _tenants;
35 private readonly IPhotoService _photos;
36
62 {
63 _tenants = tenants;
64 _photos = photos;
65 }
66
75 public TenantConfig? Config { get; private set; }
76
85 public IReadOnlyList<PhotoInfo> Gallery { get; private set; } = [];
94 public IReadOnlyList<PhotoInfo> AllPhotos { get; private set; } = [];
103 public bool IsLoaded { get; private set; }
112 public bool NotFound { get; private set; }
113
122 public string CoupleName => Config?.CoupleName ?? "";
131 public string HeroTitle => string.IsNullOrWhiteSpace(Config?.HeroTitle) ? "Thank You" : Config.HeroTitle;
140 public IReadOnlyList<string> VideoUrls => Config?.VideoFileNames
141 .Select(fn => _photos.GetVideoUrl(Config.Slug, fn))
142 .ToList() ?? [];
151 public int GalleryAutoPlayMs => Math.Clamp(Config?.GalleryAutoPlaySeconds ?? 3, 1, 30) * 1000;
160 public string Subtitle => Config?.Subtitle ?? "";
169 public DateTime WeddingDate => Config?.WeddingDate ?? DateTime.Today;
178 public string WeddingTime => Config?.WeddingTime ?? "";
187 public string VenueName => Config?.VenueName ?? "";
196 public string VenueAddress => Config?.VenueAddress ?? "";
205 public double VenueLat => Config?.VenueLat ?? 0;
214 public double VenueLng => Config?.VenueLng ?? 0;
223 public bool HasVenueCoords => VenueLat != 0 && VenueLng != 0;
232 public string Story => Config?.Story ?? "";
241 public string Story2 => Config?.Story2 ?? "";
250 public IReadOnlyList<StoryChapter> StoryChapters => Config?.StoryChapters ?? WeddingStoryChapterDefaults.Create();
259 public string ThemeName => Config?.ThemeName ?? "rose";
268 public string ThankYouStyle => Config?.ThankYouStyle ?? "onepage";
277 public WeddingLayoutMode LayoutMode => WeddingLayoutCatalog.FromLegacyKey(ThankYouStyle);
286 public IReadOnlyList<string> OrderedSections => WeddingSectionOrderCatalog.NormalizeThankYouOrder(Config?.SectionOrder);
295 public string CeremonyNoteHtml
296 {
297 get
298 {
299 var raw = Config?.CeremonyNote ?? "";
300 if (string.IsNullOrWhiteSpace(raw)) return "";
302 return raw;
303 return Markdown.ToHtml(raw, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
304 }
305 }
306
314 public bool IsCeremonyNoteHtml =>
315 string.Equals(Config?.CeremonyNoteFormat, "Html", StringComparison.OrdinalIgnoreCase);
316
325 public string HeroPanelVerticalDesktop => Config?.HeroPanelVerticalDesktop ?? "top";
334 public string HeroPanelHorizontalDesktop => Config?.HeroPanelHorizontalDesktop ?? "center";
343 public string HeroPanelVerticalMobile => Config?.HeroPanelVerticalMobile ?? "top";
352 public string HeroPanelHorizontalMobile => Config?.HeroPanelHorizontalMobile ?? "center";
361 public string HeroPanelStyle => BuildFloatingStyle(Config?.HeroPanelPlacement ?? new WeddingFloatingPosition());
371 Config?.HeroPanelPlacement.HasDesktop == true || Config?.HeroPanelPlacement.HasMobile == true;
372
381 public string HeroImageUrl
382 {
383 get
384 {
385 if (Config is null) return "";
386 if (!string.IsNullOrWhiteSpace(Config.HeroImageFileName))
387 return _photos.GetHeroUrl(Config.Slug, Config.HeroImageFileName);
388 return "";
389 }
390 }
391
400 public string RoadMapUrl
401 {
402 get
403 {
404 if (Config is null || string.IsNullOrWhiteSpace(Config.RoadMapFileName)) return "";
405 return _photos.GetRoadMapUrl(Config.Slug, Config.RoadMapFileName);
406 }
407 }
408
417 public string MapLinkKakao => Config?.MapLinkKakao ?? "";
426 public string MapLinkNaver => Config?.MapLinkNaver ?? "";
435 public string MapLinkAtlan => Config?.MapLinkAtlan ?? "";
444 public string MapLinkTmap => Config?.MapLinkTmap ?? "";
453 public bool HasMapLinks =>
454 !string.IsNullOrWhiteSpace(MapLinkKakao) ||
455 !string.IsNullOrWhiteSpace(MapLinkNaver) ||
456 !string.IsNullOrWhiteSpace(MapLinkAtlan) ||
457 !string.IsNullOrWhiteSpace(MapLinkTmap);
466 public bool HasMapSection => HasVenueCoords || !string.IsNullOrWhiteSpace(RoadMapUrl) || HasMapLinks;
475 public string SelectedTab { get; private set; } = "map";
484 public void SetMap() => SelectedTab = "map";
493 public void SetRoad() => SelectedTab = "road";
518 public string TabClass(string tab) => SelectedTab == tab ? "active" : "";
519
528 public IReadOnlyList<AccountInfo> Accounts => Config?.Accounts ?? [];
538
547 public string OgTitle => !string.IsNullOrWhiteSpace(Config?.OgTitle)
548 ? Config.OgTitle
549 : $"{CoupleName} 감사 인사";
550
559 public string OgDescription => !string.IsNullOrWhiteSpace(Config?.OgDescription)
560 ? Config.OgDescription
561 : $"{WeddingDate:yyyy년 MM월 dd일} {VenueName}에서의 결혼식을 마쳤습니다. 함께해 주셔서 감사합니다.";
562
571 public string OgImageUrl
572 {
573 get
574 {
575 if (Config is null) return "";
576 var fn = !string.IsNullOrWhiteSpace(Config.OgImageFileName)
577 ? Config.OgImageFileName
578 : Config.HeroImageFileName;
579 return string.IsNullOrWhiteSpace(fn) ? "" : _photos.GetHeroUrl(Config.Slug, fn);
580 }
581 }
582
591 public string MusicUrl
592 {
593 get
594 {
595 if (Config is null || string.IsNullOrWhiteSpace(Config.MusicFileName)) return "";
596 return _photos.GetMusicUrl(Config.Slug, Config.MusicFileName);
597 }
598 }
599
608 public string MusicButtonPosition => Config?.MusicButtonPosition ?? "bottom";
617 public string MusicButtonStyle => BuildFloatingStyle(Config?.MusicButtonPlacement ?? new WeddingFloatingPosition());
627 Config?.MusicButtonPlacement.HasDesktop == true || Config?.MusicButtonPlacement.HasMobile == true;
628
637 public bool LightboxOpen { get; private set; }
646 public int LightboxIdx { get; private set; }
647
664 public void OpenLightbox(int idx)
665 {
666 LightboxIdx = Math.Clamp(idx, 0, Math.Max(0, AllPhotos.Count - 1));
667 LightboxOpen = true;
668 }
669
677 public void CloseLightbox() => LightboxOpen = false;
686 public void LightboxNext() => LightboxIdx = (LightboxIdx + 1) % Math.Max(1, AllPhotos.Count);
695 public void LightboxPrev() => LightboxIdx = (LightboxIdx - 1 + Math.Max(1, AllPhotos.Count)) % Math.Max(1, AllPhotos.Count);
696
729 public PhotoInfo? ResolveStoryChapterPhoto(StoryChapter chapter, int chapterIndex)
730 {
731 var explicitPhoto = FindPhoto(chapter.PhotoPath) ?? FindPhoto(chapter.PhotoId);
732 if (explicitPhoto is not null)
733 {
734 return explicitPhoto;
735 }
736
737 return chapterIndex >= 0 && chapterIndex < AllPhotos.Count
738 ? AllPhotos[chapterIndex]
739 : null;
740 }
741
774 public async Task LoadAsync(string slug, CancellationToken ct = default)
775 {
776 Config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
777 if (Config is null) { NotFound = true; IsLoaded = true; return; }
779
780 var all = await _photos.GetGalleryAsync(slug, ct).ConfigureAwait(false);
781 var sorted = ApplyGalleryOrder(all, Config.GalleryFileNames);
782 AllPhotos = sorted;
783 Gallery = sorted.Take(10).ToList();
784 IsLoaded = true;
785 }
786
819 private static List<PhotoInfo> ApplyGalleryOrder(IReadOnlyList<PhotoInfo> photos, IReadOnlyList<string> order)
820 {
821 var orderMap = order
822 .Select((fileName, index) => new { fileName, index })
823 .GroupBy(x => x.fileName, StringComparer.OrdinalIgnoreCase)
824 .ToDictionary(x => x.Key, x => x.First().index, StringComparer.OrdinalIgnoreCase);
825
826 return photos
827 .OrderBy(p => orderMap.TryGetValue(p.FileName, out var index) ? index : int.MaxValue)
828 .ThenByDescending(p => p.LastModified)
829 .ThenByDescending(p => p.FileName)
830 .ToList();
831 }
832
857 private PhotoInfo? FindPhoto(string? value)
858 {
859 if (string.IsNullOrWhiteSpace(value))
860 {
861 return null;
862 }
863
864 var key = value.Trim();
865 return AllPhotos.FirstOrDefault(p =>
866 string.Equals(p.FileName, key, StringComparison.OrdinalIgnoreCase) ||
867 string.Equals(p.Url, key, StringComparison.OrdinalIgnoreCase) ||
868 string.Equals(p.ThumbUrl, key, StringComparison.OrdinalIgnoreCase) ||
869 p.Url.EndsWith("/" + key, StringComparison.OrdinalIgnoreCase) ||
870 p.ThumbUrl.EndsWith("/" + key, StringComparison.OrdinalIgnoreCase));
871 }
872
897 private static string BuildFloatingStyle(WeddingFloatingPosition position)
898 {
899 var parts = new List<string>();
900 if (position.HasDesktop)
901 {
902 parts.Add($"--w-drag-x:{ClampPercent(position.DesktopX):0.##}%;");
903 parts.Add($"--w-drag-y:{ClampPercent(position.DesktopY):0.##}%;");
904 }
905 if (position.HasMobile)
906 {
907 parts.Add($"--w-drag-mobile-x:{ClampPercent(position.MobileX):0.##}%;");
908 parts.Add($"--w-drag-mobile-y:{ClampPercent(position.MobileY):0.##}%;");
909 }
910 return string.Concat(parts);
911 }
912
937 private static double ClampPercent(double? value) => Math.Clamp(value ?? 50, 0, 100);
938
963 private static string NormalizeLinkUrl(string? value)
964 {
965 var url = value?.Trim() ?? "";
966 if (string.IsNullOrWhiteSpace(url))
967 {
968 return "";
969 }
970
971 if (url.StartsWith("/", StringComparison.Ordinal))
972 {
973 return url;
974 }
975
976 if (Uri.TryCreate(url, UriKind.Absolute, out var absolute)
977 && (absolute.Scheme == Uri.UriSchemeHttp || absolute.Scheme == Uri.UriSchemeHttps))
978 {
979 return url;
980 }
981
982 return $"https://{url}";
983 }
984 }
985}
static void Normalize(TenantConfig config)
async Task LoadAsync(string slug, CancellationToken ct=default)
InvitationViewModel(ITenantStore tenants, IPhotoService photos)
static string BuildFloatingStyle(WeddingFloatingPosition position)
PhotoInfo? ResolveStoryChapterPhoto(StoryChapter chapter, int chapterIndex)
static List< PhotoInfo > ApplyGalleryOrder(IReadOnlyList< PhotoInfo > photos, IReadOnlyList< string > order)