WeddingPlatform.Web 1.0.0.0
지도, 갤러리, 방명록, 계좌 안내와 배경음악을 링크 하나에 담는 무료 모바일 청첩장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
LocalPhotoService.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using Microsoft.AspNetCore.Components.Forms;
4
6
15public sealed class LocalPhotoService : IPhotoService
16{
25 private readonly ITenantStore _tenants;
61 private const long MaxImageUploadBytes = 200 * 1024 * 1024;
70 private static readonly string[] AllowedExts = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
71
113 ITenantStore tenants,
114 IMediaQuotaPolicyResolver policyResolver,
115 IImageOptimizationService imageOptimization,
116 IMediaUsageQueryService usageQuery)
117 {
118 _tenants = tenants;
119 _policyResolver = policyResolver;
120 _imageOptimization = imageOptimization;
121 _usageQuery = usageQuery;
122 }
123
156 public async Task<IReadOnlyList<PhotoInfo>> GetGalleryAsync(string slug, CancellationToken ct = default)
157 {
158 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
159 if (config is null) return [];
160
161 var list = new List<PhotoInfo>();
162 foreach (var fn in config.GalleryFileNames)
163 {
164 var full = GalleryPath(slug, fn);
165 if (!File.Exists(full)) continue;
166 var fi = new FileInfo(full);
167 list.Add(new PhotoInfo
168 {
169 FileName = fn,
170 Url = GetPhotoUrl(slug, fn),
171 ThumbUrl = GetThumbUrl(slug, fn),
172 SizeBytes = fi.Length,
173 LastModified = fi.LastWriteTime
174 });
175 }
176 return list;
177 }
178
219 public async Task<string> UploadAsync(string slug, IBrowserFile file, CancellationToken ct = default)
220 {
221 ValidateImageFile(file);
222 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
223 var policy = await _policyResolver.ResolveAsync(config, ct).ConfigureAwait(false);
224 await ValidateImageQuotaAsync(config, policy, file, countGalleryImage: true, ct).ConfigureAwait(false);
225
226 var baseName = Path.GetFileNameWithoutExtension(UniqueFileName(file.Name));
227 var galleryDir = Path.Combine(_tenants.GetTenantDataPath(slug), "gallery");
228 var thumbDir = Path.Combine(_tenants.GetTenantDataPath(slug), "thumb");
229 Directory.CreateDirectory(galleryDir);
230 Directory.CreateDirectory(thumbDir);
231
232 var fileName = await SaveImageFileAsync(slug, file, galleryDir, baseName, policy, ct).ConfigureAwait(false);
233 File.Copy(Path.Combine(galleryDir, fileName), Path.Combine(thumbDir, fileName), overwrite: true);
234
235 if (!config.GalleryFileNames.Contains(fileName))
236 config.GalleryFileNames.Add(fileName);
237 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
238
239 return fileName;
240 }
241
282 public async Task<string> UploadHeroAsync(string slug, IBrowserFile file, CancellationToken ct = default)
283 {
284 ValidateImageFile(file);
285 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
286 var policy = await _policyResolver.ResolveAsync(config, ct).ConfigureAwait(false);
287 await ValidateImageQuotaAsync(config, policy, file, countGalleryImage: false, ct).ConfigureAwait(false);
288
289 var dir = _tenants.GetTenantDataPath(slug);
290 Directory.CreateDirectory(dir);
291
292 var fileName = await SaveImageFileAsync(slug, file, dir, "hero", policy, ct).ConfigureAwait(false);
293 config.HeroImageFileName = fileName;
294 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
295
296 return fileName;
297 }
298
339 public async Task<string> UploadRoadMapAsync(string slug, IBrowserFile file, CancellationToken ct = default)
340 {
341 ValidateImageFile(file);
342 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
343 var policy = await _policyResolver.ResolveAsync(config, ct).ConfigureAwait(false);
344 await ValidateImageQuotaAsync(config, policy, file, countGalleryImage: false, ct).ConfigureAwait(false);
345
346 var dir = _tenants.GetTenantDataPath(slug);
347 Directory.CreateDirectory(dir);
348
349 var fileName = await SaveImageFileAsync(slug, file, dir, "roadmap", policy, ct).ConfigureAwait(false);
350 config.RoadMapFileName = fileName;
351 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
352
353 return fileName;
354 }
355
396 public async Task DeleteAsync(string slug, string fileName, CancellationToken ct = default)
397 {
398 var galleryFile = GalleryPath(slug, fileName);
399 var thumbFile = Path.Combine(_tenants.GetTenantDataPath(slug), "thumb", fileName);
400 if (File.Exists(galleryFile)) File.Delete(galleryFile);
401 if (File.Exists(thumbFile)) File.Delete(thumbFile);
402
403 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
404 if (config is null) return;
405 config.GalleryFileNames.Remove(fileName);
406 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
407 }
408
441 public string GetPhotoUrl(string slug, string fileName) =>
442 $"/wedding-data/{slug}/gallery/{Uri.EscapeDataString(fileName)}";
443
476 public string GetThumbUrl(string slug, string fileName) =>
477 $"/wedding-data/{slug}/thumb/{Uri.EscapeDataString(fileName)}";
478
511 public string GetHeroUrl(string slug, string fileName) =>
512 $"/wedding-data/{slug}/{Uri.EscapeDataString(fileName)}";
513
546 public string GetRoadMapUrl(string slug, string fileName) =>
547 $"/wedding-data/{slug}/{Uri.EscapeDataString(fileName)}";
548
597 public async Task<string> UploadMusicAsync(string slug, IBrowserFile file, CancellationToken ct = default)
598 {
599 var ext = Path.GetExtension(file.Name).ToLowerInvariant();
600 if (ext is not (".mp3" or ".ogg" or ".wav" or ".aac" or ".m4a"))
601 throw new InvalidOperationException($"허용되지 않는 오디오 형식입니다: {ext}");
602 if (file.Size > 30 * 1024 * 1024)
603 throw new InvalidOperationException("음악 파일은 30MB 이하여야 합니다.");
604
605 var dir = _tenants.GetTenantDataPath(slug);
606 Directory.CreateDirectory(dir);
607
608 var fileName = $"bgmusic{ext}";
609 var destPath = Path.Combine(dir, fileName);
610 await using (var dest = File.Create(destPath))
611 await file.OpenReadStream(30 * 1024 * 1024, ct).CopyToAsync(dest, ct).ConfigureAwait(false);
612
613 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
614 config.MusicFileName = fileName;
615 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
616
617 return fileName;
618 }
619
652 public string GetMusicUrl(string slug, string fileName) =>
653 $"/wedding-data/{slug}/{Uri.EscapeDataString(fileName)}";
654
695 public async Task<string> UploadOgImageAsync(string slug, IBrowserFile file, CancellationToken ct = default)
696 {
697 ValidateImageFile(file);
698 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
699 var policy = await _policyResolver.ResolveAsync(config, ct).ConfigureAwait(false);
700 await ValidateImageQuotaAsync(config, policy, file, countGalleryImage: false, ct).ConfigureAwait(false);
701
702 var dir = _tenants.GetTenantDataPath(slug);
703 Directory.CreateDirectory(dir);
704
705 var fileName = await SaveImageFileAsync(slug, file, dir, "og", policy, ct).ConfigureAwait(false);
706 config.OgImageFileName = fileName;
707 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
708
709 return fileName;
710 }
711
752 public async Task<string> UploadThankYouOgImageAsync(string slug, IBrowserFile file, CancellationToken ct = default)
753 {
754 ValidateImageFile(file);
755 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
756 var policy = await _policyResolver.ResolveAsync(config, ct).ConfigureAwait(false);
757 await ValidateImageQuotaAsync(config, policy, file, countGalleryImage: false, ct).ConfigureAwait(false);
758
759 var dir = _tenants.GetTenantDataPath(slug);
760 Directory.CreateDirectory(dir);
761
762 var fileName = await SaveImageFileAsync(slug, file, dir, "og-thankyou", policy, ct).ConfigureAwait(false);
763 config.ThankYouOgImageFileName = fileName;
764 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
765
766 return fileName;
767 }
768
817 public async Task<string> UploadVideoAsync(string slug, IBrowserFile file, CancellationToken ct = default)
818 {
819 var ext = Path.GetExtension(file.Name).ToLowerInvariant();
820 if (ext is not (".mp4" or ".webm" or ".mov" or ".m4v"))
821 throw new InvalidOperationException($"허용되지 않는 동영상 형식입니다: {ext}. mp4/webm/mov 권장");
822
823 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new TenantConfig { Slug = slug };
824 var policy = await _policyResolver.ResolveAsync(config, ct).ConfigureAwait(false);
825
826 var effectiveCount = policy.VideoMaxCount;
827 if (effectiveCount > 0 && config.VideoFileNames.Count >= effectiveCount)
828 throw new InvalidOperationException($"동영상은 최대 {effectiveCount}개까지 업로드할 수 있습니다.");
829
830 var effectiveMb = policy.VideoMaxFileSizeMb;
831 var maxBytes = policy.VideoMaxFileSizeBytes;
832
833 if (file.Size > maxBytes)
834 throw new InvalidOperationException($"동영상 파일은 {effectiveMb}MB 이하여야 합니다.");
835
836 if (policy.VideoMaxStorageMb > 0)
837 {
838 var usage = await _usageQuery.GetTenantUsageAsync(config, ct).ConfigureAwait(false);
839 if (usage.VideoBytes + file.Size > policy.VideoMaxStorageBytes)
840 {
841 throw new InvalidOperationException($"동영상 저장 용량은 최대 {policy.VideoMaxStorageMb}MB까지 사용할 수 있습니다.");
842 }
843 }
844
845 var dir = _tenants.GetTenantDataPath(slug);
846 Directory.CreateDirectory(dir);
847
848 var fileName = $"video-{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid().ToString("N")[..8]}{ext}";
849 var destPath = Path.Combine(dir, fileName);
850 await using (var dest = File.Create(destPath))
851 await file.OpenReadStream(maxBytes, ct).CopyToAsync(dest, ct).ConfigureAwait(false);
852
853 config.VideoFileNames.Add(fileName);
854 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
855
856 return fileName;
857 }
858
899 public async Task DeleteVideoAsync(string slug, string fileName, CancellationToken ct = default)
900 {
901 var path = Path.Combine(_tenants.GetTenantDataPath(slug), fileName);
902 if (File.Exists(path)) File.Delete(path);
903
904 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
905 if (config is null) return;
906 config.VideoFileNames.Remove(fileName);
907 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
908 }
909
942 public string GetVideoUrl(string slug, string fileName) =>
943 $"/wedding-data/{slug}/{Uri.EscapeDataString(fileName)}";
944
977 private string GalleryPath(string slug, string fileName) =>
978 Path.Combine(_tenants.GetTenantDataPath(slug), "gallery", fileName);
979
1044 private async Task ValidateImageQuotaAsync(TenantConfig config, EffectiveMediaPolicy policy, IBrowserFile file, bool countGalleryImage, CancellationToken ct)
1045 {
1046 if (countGalleryImage && policy.ImageMaxCount > 0 && config.GalleryFileNames.Count >= policy.ImageMaxCount)
1047 {
1048 throw new InvalidOperationException($"이미지는 최대 {policy.ImageMaxCount}장까지 업로드할 수 있습니다.");
1049 }
1050
1051 if (policy.ImageOptimizedMaxStorageMb > 0)
1052 {
1053 var usage = await _usageQuery.GetTenantUsageAsync(config, ct).ConfigureAwait(false);
1054 if (usage.OptimizedImageBytes + file.Size > policy.ImageOptimizedMaxStorageBytes)
1055 {
1056 throw new InvalidOperationException($"이미지 저장 용량은 최대 {policy.ImageOptimizedMaxStorageMb}MB까지 사용할 수 있습니다.");
1057 }
1058 }
1059 }
1060
1125 private async Task<string> SaveImageFileAsync(string slug, IBrowserFile file, string destinationDirectory, string baseName, EffectiveMediaPolicy policy, CancellationToken ct)
1126 {
1127 var root = _tenants.GetTenantDataPath(slug);
1128 var originalDir = Path.Combine(root, "original");
1129 var originalExt = Path.GetExtension(file.Name).ToLowerInvariant();
1130 var tempPath = Path.Combine(destinationDirectory, $"{baseName}.upload{originalExt}");
1131 Directory.CreateDirectory(destinationDirectory);
1132
1133 await using (var dest = File.Create(tempPath))
1134 {
1135 await file.OpenReadStream(MaxImageUploadBytes, ct).CopyToAsync(dest, ct).ConfigureAwait(false);
1136 }
1137
1138 if (policy.KeepOriginalImages)
1139 {
1140 Directory.CreateDirectory(originalDir);
1141 File.Copy(tempPath, Path.Combine(originalDir, $"{baseName}{originalExt}"), overwrite: true);
1142 }
1143
1144 var outputFormat = ResolveWritableOutputFormat(policy.ImageOutputFormat, originalExt);
1145 if (!string.IsNullOrWhiteSpace(outputFormat))
1146 {
1147 var outputExt = $".{outputFormat}";
1148 var optimizedName = $"{baseName}{outputExt}";
1149 var optimizedPath = Path.Combine(destinationDirectory, optimizedName);
1150 var tempOptimizedPath = Path.Combine(destinationDirectory, $"{baseName}.optimized.tmp{outputExt}");
1151 var result = await _imageOptimization.OptimizeAsync(tempPath, tempOptimizedPath, policy, ct).ConfigureAwait(false);
1152 if (result.Succeeded && File.Exists(tempOptimizedPath))
1153 {
1154 File.Move(tempOptimizedPath, optimizedPath, overwrite: true);
1155 File.Delete(tempPath);
1156 return optimizedName;
1157 }
1158
1159 if (File.Exists(tempOptimizedPath))
1160 {
1161 File.Delete(tempOptimizedPath);
1162 }
1163 }
1164
1165 var fallbackName = $"{baseName}{originalExt}";
1166 var fallbackPath = Path.Combine(destinationDirectory, fallbackName);
1167 File.Move(tempPath, fallbackPath, overwrite: true);
1168 return fallbackName;
1169 }
1170
1203 private string ResolveWritableOutputFormat(string? preferredFormat, string? originalExtension)
1204 {
1205 var preferred = NormalizeOutputExtension(preferredFormat);
1206 if (_imageOptimization.CanEncode(preferred)) return preferred;
1207
1208 if (_imageOptimization.CanEncode("jpg")) return "jpg";
1209
1210 var original = NormalizeOutputExtension(originalExtension);
1211 return _imageOptimization.CanEncode(original) ? original : "";
1212 }
1213
1238 private static void ValidateImageFile(IBrowserFile file)
1239 {
1240 var ext = Path.GetExtension(file.Name).ToLowerInvariant();
1241 if (!Array.Exists(AllowedExts, e => e == ext))
1242 throw new InvalidOperationException($"허용되지 않는 파일 형식입니다: {ext}");
1243 if (file.Size > MaxImageUploadBytes)
1244 throw new InvalidOperationException("이미지 파일 크기는 200MB 이하여야 합니다.");
1245 }
1246
1271 private static string NormalizeOutputExtension(string? outputFormat)
1272 {
1273 var normalized = outputFormat?.Trim().TrimStart('.').ToLowerInvariant();
1274 return string.IsNullOrWhiteSpace(normalized) ? "webp" : normalized;
1275 }
1276
1301 private static string UniqueFileName(string originalName)
1302 {
1303 var ext = Path.GetExtension(originalName).ToLowerInvariant();
1304 return $"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{ext}";
1305 }
1306}
string ResolveWritableOutputFormat(string? preferredFormat, string? originalExtension)
async Task DeleteAsync(string slug, string fileName, CancellationToken ct=default)
string GetThumbUrl(string slug, string fileName)
string GalleryPath(string slug, string fileName)
LocalPhotoService(ITenantStore tenants, IMediaQuotaPolicyResolver policyResolver, IImageOptimizationService imageOptimization, IMediaUsageQueryService usageQuery)
async Task< string > UploadAsync(string slug, IBrowserFile file, CancellationToken ct=default)
string GetRoadMapUrl(string slug, string fileName)
string GetHeroUrl(string slug, string fileName)
string GetMusicUrl(string slug, string fileName)
static string UniqueFileName(string originalName)
readonly IImageOptimizationService _imageOptimization
string GetVideoUrl(string slug, string fileName)
async Task< string > UploadMusicAsync(string slug, IBrowserFile file, CancellationToken ct=default)
static string NormalizeOutputExtension(string? outputFormat)
async Task< string > UploadOgImageAsync(string slug, IBrowserFile file, CancellationToken ct=default)
async Task< string > UploadThankYouOgImageAsync(string slug, IBrowserFile file, CancellationToken ct=default)
async Task< string > UploadRoadMapAsync(string slug, IBrowserFile file, CancellationToken ct=default)
async Task ValidateImageQuotaAsync(TenantConfig config, EffectiveMediaPolicy policy, IBrowserFile file, bool countGalleryImage, CancellationToken ct)
async Task< IReadOnlyList< PhotoInfo > > GetGalleryAsync(string slug, CancellationToken ct=default)
readonly IMediaUsageQueryService _usageQuery
string GetPhotoUrl(string slug, string fileName)
readonly IMediaQuotaPolicyResolver _policyResolver
async Task< string > UploadVideoAsync(string slug, IBrowserFile file, CancellationToken ct=default)
static void ValidateImageFile(IBrowserFile file)
async Task DeleteVideoAsync(string slug, string fileName, CancellationToken ct=default)
async Task< string > SaveImageFileAsync(string slug, IBrowserFile file, string destinationDirectory, string baseName, EffectiveMediaPolicy policy, CancellationToken ct)
async Task< string > UploadHeroAsync(string slug, IBrowserFile file, CancellationToken ct=default)