WeddingPlatform.Web 1.0.0.0
지도, 갤러리, 방명록, 계좌 안내와 배경음악을 링크 하나에 담는 무료 모바일 청첩장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
WeddingInvitationViewModel.cs
이 파일의 문서화 페이지로 가기
1using Markdig;
2using Wedding.Common;
5
7
16public sealed class WeddingInvitationViewModel
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; }
84 public IReadOnlyList<PhotoInfo> Gallery { get; private set; } = [];
93 public IReadOnlyList<PhotoInfo> AllPhotos { get; private set; } = [];
102 public bool IsLoaded { get; private set; }
111 public bool NotFound { get; private set; }
112
121 public string CoupleName => Config?.CoupleName ?? "";
130 public string HeroTitle => string.IsNullOrWhiteSpace(Config?.HeroTitle) ? "Save The Date" : Config.HeroTitle;
139 public IReadOnlyList<string> VideoUrls => Config?.VideoFileNames
140 .Select(fn => _photos.GetVideoUrl(Config.Slug, fn))
141 .ToList() ?? [];
150 public int GalleryAutoPlayMs => Math.Clamp(Config?.GalleryAutoPlaySeconds ?? 3, 1, 30) * 1000;
159 public string Subtitle => Config?.Subtitle ?? "";
168 public DateTime WeddingDate => Config?.WeddingDate ?? DateTime.Today;
177 public string WeddingTime => Config?.WeddingTime ?? "";
186 public string VenueName => Config?.VenueName ?? "";
195 public string VenueAddress => Config?.VenueAddress ?? "";
204 public string Story => Config?.Story ?? "";
213 public string Story2 => Config?.Story2 ?? "";
222 public WeddingSiteMode Mode => Config?.Mode ?? WeddingSiteMode.Invite;
231 public bool ShowThankYouLink => Mode == WeddingSiteMode.Both;
240 public string ThankYouUrl => Config?.ThankYouUrl ?? "";
249 public string MapLinkKakao => Config?.MapLinkKakao ?? "";
258 public string MapLinkNaver => Config?.MapLinkNaver ?? "";
267 public string MapLinkAtlan => Config?.MapLinkAtlan ?? "";
276 public string MapLinkTmap => Config?.MapLinkTmap ?? "";
285 public double VenueLat => Config?.VenueLat ?? 0;
294 public double VenueLng => Config?.VenueLng ?? 0;
303 public bool HasVenueCoords => VenueLat != 0 && VenueLng != 0;
312 public DesignSettings DesignSettings => Config?.DesignSettings ?? new DesignSettings();
321 public IReadOnlyList<StoryChapter> StoryChapters => DesignSettings.StoryChapters;
339 public WeddingLayoutMode LayoutMode => DesignSettings.LayoutMode == WeddingLayoutMode.Unknown
341 : DesignSettings.LayoutMode;
368 public bool UsesBottomNavigation => LayoutDescriptor.UsesBottomNavigation;
377 public IReadOnlyList<string> OrderedSections =>
378 WeddingSectionOrderCatalog.NormalizeInvitationOrder(DesignSettings.SectionOrder, LayoutDescriptor.SupportedSections);
387 public string CeremonyNoteHtml
388 {
389 get
390 {
391 var raw = Config?.CeremonyNote ?? "";
392 if (string.IsNullOrWhiteSpace(raw)) return "";
394 return raw;
395 return Markdown.ToHtml(raw, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
396 }
397 }
398
406 public bool IsCeremonyNoteHtml =>
407 string.Equals(Config?.CeremonyNoteFormat, "Html", StringComparison.OrdinalIgnoreCase);
408
417 public string HeroImageUrl
418 {
419 get
420 {
421 if (Config is null) return "";
422 if (!string.IsNullOrWhiteSpace(Config.HeroImageFileName))
423 return _photos.GetHeroUrl(Config.Slug, Config.HeroImageFileName);
424 return "";
425 }
426 }
427
436 public string RoadMapUrl
437 {
438 get
439 {
440 if (Config is null || string.IsNullOrWhiteSpace(Config.RoadMapFileName)) return "";
441 return _photos.GetRoadMapUrl(Config.Slug, Config.RoadMapFileName);
442 }
443 }
444
453 public IReadOnlyList<AccountInfo> Accounts => Config?.Accounts ?? [];
454
463 public string OgTitle => !string.IsNullOrWhiteSpace(Config?.OgTitle)
464 ? Config.OgTitle
465 : $"{CoupleName} 청첩장";
466
475 public string OgDescription => !string.IsNullOrWhiteSpace(Config?.OgDescription)
476 ? Config.OgDescription
477 : $"{WeddingDate:yyyy년 MM월 dd일} {VenueName}에서 함께해 주세요.";
478
487 public string OgImageUrl
488 {
489 get
490 {
491 if (Config is null) return "";
492 // OG 전용 이미지 우선, 없으면 히어로 이미지
493 var fn = !string.IsNullOrWhiteSpace(Config.OgImageFileName)
494 ? Config.OgImageFileName
495 : Config.HeroImageFileName;
496 return string.IsNullOrWhiteSpace(fn) ? "" : _photos.GetHeroUrl(Config.Slug, fn);
497 }
498 }
499
508 public string ThankYouOgTitle => !string.IsNullOrWhiteSpace(Config?.ThankYouOgTitle)
509 ? Config.ThankYouOgTitle
510 : $"{CoupleName} 감사 인사";
511
520 public string ThankYouOgDescription => !string.IsNullOrWhiteSpace(Config?.ThankYouOgDescription)
521 ? Config.ThankYouOgDescription
522 : $"{WeddingDate:yyyy년 MM월 dd일} {VenueName}에서의 결혼식을 마쳤습니다. 함께해 주셔서 감사합니다.";
523
532 public string ThankYouOgImageUrl
533 {
534 get
535 {
536 if (Config is null) return "";
537 var fn = !string.IsNullOrWhiteSpace(Config.ThankYouOgImageFileName)
538 ? Config.ThankYouOgImageFileName
539 : Config.HeroImageFileName;
540 return string.IsNullOrWhiteSpace(fn) ? "" : _photos.GetHeroUrl(Config.Slug, fn);
541 }
542 }
543
552 public string MusicUrl
553 {
554 get
555 {
556 if (Config is null || string.IsNullOrWhiteSpace(Config.MusicFileName)) return "";
557 return _photos.GetMusicUrl(Config.Slug, Config.MusicFileName);
558 }
559 }
560
569 public string MusicButtonPosition => Config?.MusicButtonPosition ?? "bottom";
578 public string MusicButtonStyle => BuildFloatingStyle(DesignSettings.MusicButtonPlacement);
588 DesignSettings.MusicButtonPlacement.HasDesktop || DesignSettings.MusicButtonPlacement.HasMobile;
589
598 public string HeroPanelVerticalDesktop => NormalizeOption(DesignSettings.HeroPlacement.ThankYou.DesktopVertical, ["top", "middle", "bottom"], "top");
607 public string HeroPanelHorizontalDesktop => NormalizeOption(DesignSettings.HeroPlacement.ThankYou.DesktopHorizontal, ["left", "center", "right"], "center");
616 public string HeroPanelVerticalMobile => NormalizeOption(DesignSettings.HeroPlacement.ThankYou.MobileVertical, ["top", "middle", "bottom"], "top");
625 public string HeroPanelHorizontalMobile => NormalizeOption(DesignSettings.HeroPlacement.ThankYou.MobileHorizontal, ["left", "center", "right"], "center");
634 public string InviteHeroTopVerticalDesktop => NormalizeOption(DesignSettings.HeroPlacement.InviteTop.DesktopVertical, ["top", "middle", "bottom"], "top");
643 public string InviteHeroTopHorizontalDesktop => NormalizeOption(DesignSettings.HeroPlacement.InviteTop.DesktopHorizontal, ["left", "center", "right"], "center");
652 public string InviteHeroTopVerticalMobile => NormalizeOption(DesignSettings.HeroPlacement.InviteTop.MobileVertical, ["top", "middle", "bottom"], "top");
661 public string InviteHeroTopHorizontalMobile => NormalizeOption(DesignSettings.HeroPlacement.InviteTop.MobileHorizontal, ["left", "center", "right"], "center");
670 public string InviteHeroBottomVerticalDesktop => NormalizeOption(DesignSettings.HeroPlacement.InviteBottom.DesktopVertical, ["top", "middle", "bottom"], "bottom");
679 public string InviteHeroBottomHorizontalDesktop => NormalizeOption(DesignSettings.HeroPlacement.InviteBottom.DesktopHorizontal, ["left", "center", "right"], "center");
688 public string InviteHeroBottomVerticalMobile => NormalizeOption(DesignSettings.HeroPlacement.InviteBottom.MobileVertical, ["top", "middle", "bottom"], "bottom");
697 public string InviteHeroBottomHorizontalMobile => NormalizeOption(DesignSettings.HeroPlacement.InviteBottom.MobileHorizontal, ["left", "center", "right"], "center");
706 public string InviteHeroTopStyle => BuildHeroPanelStyle(DesignSettings.HeroPlacement.InviteTop);
715 public string InviteHeroBottomStyle => BuildHeroPanelStyle(DesignSettings.HeroPlacement.InviteBottom);
725 DesignSettings.HeroPlacement.InviteTop.HasDesktopCustomPosition || DesignSettings.HeroPlacement.InviteTop.HasMobileCustomPosition;
735 DesignSettings.HeroPlacement.InviteBottom.HasDesktopCustomPosition || DesignSettings.HeroPlacement.InviteBottom.HasMobileCustomPosition;
736
745 public string SelectedTab { get; private set; } = "map";
754 public void SetMap() => SelectedTab = "map";
763 public void SetRoad() => SelectedTab = "road";
788 public string TabClass(string tab) => SelectedTab == tab ? "active" : "";
789
798 public bool LightboxOpen { get; private set; }
807 public int LightboxIdx { get; private set; }
808
825 public void OpenLightbox(int idx)
826 {
827 LightboxIdx = Math.Clamp(idx, 0, Math.Max(0, AllPhotos.Count - 1));
828 LightboxOpen = true;
829 }
830
838 public void CloseLightbox() => LightboxOpen = false;
847 public void LightboxNext() => LightboxIdx = (LightboxIdx + 1) % Math.Max(1, AllPhotos.Count);
856 public void LightboxPrev() => LightboxIdx = (LightboxIdx - 1 + Math.Max(1, AllPhotos.Count)) % Math.Max(1, AllPhotos.Count);
857
890 public PhotoInfo? ResolveStoryChapterPhoto(StoryChapter chapter, int chapterIndex)
891 {
892 var explicitPhoto = FindPhoto(chapter.PhotoPath) ?? FindPhoto(chapter.PhotoId);
893 if (explicitPhoto is not null)
894 {
895 return explicitPhoto;
896 }
897
898 return chapterIndex >= 0 && chapterIndex < AllPhotos.Count
899 ? AllPhotos[chapterIndex]
900 : null;
901 }
902
935 public async Task LoadAsync(string slug, CancellationToken ct = default)
936 {
937 Config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
938 if (Config is null) { NotFound = true; IsLoaded = true; return; }
940
941 var all = await _photos.GetGalleryAsync(slug, ct).ConfigureAwait(false);
942 var sorted = ApplyGalleryOrder(all, Config.GalleryFileNames);
943 AllPhotos = sorted;
944 Gallery = sorted.Take(10).ToList();
945 IsLoaded = true;
946 }
947
980 private static List<PhotoInfo> ApplyGalleryOrder(IReadOnlyList<PhotoInfo> photos, IReadOnlyList<string> order)
981 {
982 var orderMap = order
983 .Select((fileName, index) => new { fileName, index })
984 .GroupBy(x => x.fileName, StringComparer.OrdinalIgnoreCase)
985 .ToDictionary(x => x.Key, x => x.First().index, StringComparer.OrdinalIgnoreCase);
986
987 return photos
988 .OrderBy(p => orderMap.TryGetValue(p.FileName, out var index) ? index : int.MaxValue)
989 .ThenByDescending(p => p.LastModified)
990 .ThenByDescending(p => p.FileName)
991 .ToList();
992 }
993
1018 private PhotoInfo? FindPhoto(string? value)
1019 {
1020 if (string.IsNullOrWhiteSpace(value))
1021 {
1022 return null;
1023 }
1024
1025 var key = value.Trim();
1026 return AllPhotos.FirstOrDefault(p =>
1027 string.Equals(p.FileName, key, StringComparison.OrdinalIgnoreCase) ||
1028 string.Equals(p.Url, key, StringComparison.OrdinalIgnoreCase) ||
1029 string.Equals(p.ThumbUrl, key, StringComparison.OrdinalIgnoreCase) ||
1030 p.Url.EndsWith("/" + key, StringComparison.OrdinalIgnoreCase) ||
1031 p.ThumbUrl.EndsWith("/" + key, StringComparison.OrdinalIgnoreCase));
1032 }
1033
1074 private static string NormalizeOption(string? value, string[] allowed, string fallback)
1075 {
1076 var normalized = value?.Trim().ToLowerInvariant();
1077 return !string.IsNullOrWhiteSpace(normalized) && allowed.Contains(normalized) ? normalized : fallback;
1078 }
1079
1104 private static string BuildFloatingStyle(WeddingFloatingPosition position)
1105 {
1106 var parts = new List<string>();
1107 if (position.HasDesktop)
1108 {
1109 parts.Add($"--w-drag-x:{ClampPercent(position.DesktopX):0.##}%;");
1110 parts.Add($"--w-drag-y:{ClampPercent(position.DesktopY):0.##}%;");
1111 }
1112 if (position.HasMobile)
1113 {
1114 parts.Add($"--w-drag-mobile-x:{ClampPercent(position.MobileX):0.##}%;");
1115 parts.Add($"--w-drag-mobile-y:{ClampPercent(position.MobileY):0.##}%;");
1116 }
1117 return string.Concat(parts);
1118 }
1119
1144 private static string BuildHeroPanelStyle(HeroPanelPlacement placement)
1145 {
1146 var parts = new List<string>();
1147 if (placement.HasDesktopCustomPosition)
1148 {
1149 parts.Add($"--w-drag-x:{ClampPercent(placement.DesktopX):0.##}%;");
1150 parts.Add($"--w-drag-y:{ClampPercent(placement.DesktopY):0.##}%;");
1151 }
1152 if (placement.HasMobileCustomPosition)
1153 {
1154 parts.Add($"--w-drag-mobile-x:{ClampPercent(placement.MobileX):0.##}%;");
1155 parts.Add($"--w-drag-mobile-y:{ClampPercent(placement.MobileY):0.##}%;");
1156 }
1157 return string.Concat(parts);
1158 }
1159
1184 private static double ClampPercent(double? value) => Math.Clamp(value ?? 50, 0, 100);
1185}
static WeddingLayoutMode FromLegacyLayoutKey(string? key)
static string ToLegacyLayoutKey(WeddingLayoutMode mode)
static WeddingThemeOption GetTheme(string? key)
static void Normalize(TenantConfig config)
static WeddingLayoutOption GetLayout(WeddingLayoutMode mode)
static string NormalizeOption(string? value, string[] allowed, string fallback)
static string BuildFloatingStyle(WeddingFloatingPosition position)
static string BuildHeroPanelStyle(HeroPanelPlacement placement)
WeddingInvitationViewModel(ITenantStore tenants, IPhotoService photos)
PhotoInfo? ResolveStoryChapterPhoto(StoryChapter chapter, int chapterIndex)
static List< PhotoInfo > ApplyGalleryOrder(IReadOnlyList< PhotoInfo > photos, IReadOnlyList< string > order)
async Task LoadAsync(string slug, CancellationToken ct=default)