Portfolio.Web 1.0.0.0
.NET, Blazor, WPF와 서비스 운영 경험을 프로젝트·이력·기술 스택 단위로 보여주는 개발자 포트폴리오입니다.
로딩중...
검색중...
일치하는것 없음
PortfolioPublicViewModel.cs
이 파일의 문서화 페이지로 가기
3
5
15{
33 private readonly IProjectStore _projects;
42 private readonly IResumeStore _resumes;
51 private readonly IContactStore _contacts;
60 private readonly IMediaService _media;
61
70 public PortfolioConfig? Config { get; private set; }
79 public List<ProjectItem> Projects { get; private set; } = [];
88 public ResumeInfo Resume { get; private set; } = new();
97 public bool HasLoaded { get; private set; }
106 public string StatusMessage { get; set; } = "";
107
108 // 연락처 폼
117 public string ContactName { get; set; } = "";
126 public string ContactEmail { get; set; } = "";
135 public string ContactMessage { get; set; } = "";
136
137 // 필터/검색
146 public string Query { get; set; } = "";
155 public string SelectedTag { get; set; } = "";
164 public string SelectedCategory { get; set; } = "";
165
174 public List<ProjectItem> Personal => FilteredProjects.Where(p => p.Category == ProjectCategory.Personal).ToList();
183 public List<ProjectItem> Work => FilteredProjects.Where(p => p.Category == ProjectCategory.Work).ToList();
192 public List<ProjectItem> Public => FilteredProjects.Where(p => p.Category == ProjectCategory.Public).ToList();
193
202 public IEnumerable<string> AllTags => Projects
203 .SelectMany(p => p.Tags)
204 .Distinct()
205 .OrderBy(t => t);
206
215 private List<ProjectItem> FilteredProjects
216 {
217 get
218 {
219 var q = Projects.AsEnumerable();
220 if (!string.IsNullOrWhiteSpace(Query))
221 {
222 var lq = Query.ToLowerInvariant();
223 q = q.Where(p => p.Title.ToLower().Contains(lq)
224 || p.Description.ToLower().Contains(lq)
225 || p.Tags.Any(t => t.ToLower().Contains(lq)));
226 }
227 if (!string.IsNullOrWhiteSpace(SelectedTag))
228 q = q.Where(p => p.Tags.Contains(SelectedTag));
229 if (!string.IsNullOrWhiteSpace(SelectedCategory) &&
230 Enum.TryParse<ProjectCategory>(SelectedCategory, out var cat))
231 q = q.Where(p => p.Category == cat);
232 return q.ToList();
233 }
234 }
235
285 IPortfolioTenantStore tenants,
286 IProjectStore projects,
287 IResumeStore resumes,
288 IContactStore contacts,
289 IMediaService media)
290 {
291 _tenants = tenants;
292 _projects = projects;
293 _resumes = resumes;
294 _contacts = contacts;
295 _media = media;
296 }
297
322 public async Task LoadAsync(string slug)
323 {
324 HasLoaded = false;
325 Config = null;
326 Projects = [];
327 Resume = new();
328
329 Config = await _tenants.GetAsync(slug);
330 if (Config == null)
331 {
332 HasLoaded = true;
333 return;
334 }
335
336 Projects = await _projects.GetAllAsync(slug);
337 Resume = await _resumes.GetAsync(slug);
338 // 기존 데이터 마이그레이션: ImageFileName → WorkImages (표시 전용, 저장 안 함)
339 foreach (var p in Projects.Where(p =>
340 !string.IsNullOrWhiteSpace(p.ImageFileName) && p.WorkImages.Length == 0))
341 p.WorkImages = [p.ImageFileName!];
342
343 HasLoaded = true;
344 }
345
386 public string GetMediaUrl(string slug, string projectId, string fileName) =>
387 _media.GetMediaUrl(slug, projectId, fileName);
388
429 public string GetImageUrl(string slug, string projectId, string fileName) =>
430 IsExternalUrl(fileName) ? fileName : _media.GetMediaUrl(slug, projectId, fileName);
431
472 public string GetVideoUrl(string slug, string projectId, string fileName) =>
473 IsExternalUrl(fileName) ? fileName : _media.GetMediaUrl(slug, projectId, fileName);
474
499 public static bool IsYouTube(string url) =>
500 url.Contains("youtube.com") || url.Contains("youtu.be");
501
526 public static string GetYouTubeEmbedUrl(string url)
527 {
528 // youtu.be/ID 또는 youtube.com/watch?v=ID → /embed/ID
529 var id = "";
530 if (url.Contains("youtu.be/"))
531 id = url.Split("youtu.be/")[1].Split('?')[0].Split('&')[0];
532 else if (url.Contains("v="))
533 id = url.Split("v=")[1].Split('&')[0].Split('?')[0];
534 return string.IsNullOrWhiteSpace(id) ? url : $"https://www.youtube.com/embed/{id}";
535 }
536
561 private static bool IsExternalUrl(string url) =>
562 url.StartsWith('/') || url.StartsWith("http://") || url.StartsWith("https://");
563
596 public string GetProfileImageUrl(string slug, string fileName) =>
597 _media.GetProfileImageUrl(slug, fileName);
598
623 public async Task<bool> SendContactAsync(string slug)
624 {
625 StatusMessage = "";
626 if (string.IsNullOrWhiteSpace(ContactName)) { StatusMessage = "❌ 이름을 입력하세요."; return false; }
627 if (string.IsNullOrWhiteSpace(ContactMessage)) { StatusMessage = "❌ 메시지를 입력하세요."; return false; }
628
629 var msg = new ContactMessage
630 {
631 SenderName = ContactName.Trim(),
632 Email = ContactEmail.Trim(),
633 Message = ContactMessage.Trim(),
634 SentAt = DateTime.Now,
635 };
636 await _contacts.SaveAsync(slug, msg);
638 StatusMessage = "✅ 메시지가 전송되었습니다.";
639 return true;
640 }
641}
string GetVideoUrl(string slug, string projectId, string fileName)
PortfolioPublicViewModel(IPortfolioTenantStore tenants, IProjectStore projects, IResumeStore resumes, IContactStore contacts, IMediaService media)
string GetImageUrl(string slug, string projectId, string fileName)
string GetMediaUrl(string slug, string projectId, string fileName)
string GetProfileImageUrl(string slug, string fileName)