2using Dreamine.Identity;
3using Microsoft.AspNetCore.Components.Forms;
182 public List<ProjectItem>
Projects {
get;
private set; } = [];
200 public List<ContactMessage>
Messages {
get;
private set; } = [];
372 var cfg = await
_tenants.GetAsync(slug).ConfigureAwait(
false);
378 var user = await
_userContext.GetCurrentAsync().ConfigureAwait(
false);
386 await
LoadAsync(slug).ConfigureAwait(
false);
416 var cfg = await
_tenants.GetAsync(slug);
417 if (cfg ==
null) {
StatusMessage =
"❌ 존재하지 않는 포트폴리오입니다.";
return false; }
419 var user = await
_userContext.GetCurrentAsync().ConfigureAwait(
false);
432 var verification = DreaminePasswordHasher.VerifyPassword(
LoginPassword, cfg.PasswordHash, out var upgradedHash);
433 if (verification is PasswordHashVerificationResult.Failed) {
StatusMessage =
"❌ 비밀번호가 틀렸습니다.";
return false; }
436 if (verification is PasswordHashVerificationResult.SuccessRehashNeeded && upgradedHash is not
null)
438 cfg.PasswordHash = upgradedHash;
439 await
_tenants.SaveAsync(cfg).ConfigureAwait(
false);
442 if (user.IsAuthenticated &&
string.IsNullOrWhiteSpace(cfg.OwnerUserId))
444 cfg.OwnerUserId = user.Id;
445 cfg.OwnerProvider = user.Provider;
446 cfg.OwnerEmail = user.Email;
447 cfg.OwnerDisplayName = user.DisplayName;
448 cfg.OwnerLinkedAt = DateTime.Now;
450 await
_tenants.SaveAsync(cfg).ConfigureAwait(
false);
456 else if (user.IsAuthenticated)
459 await
_tenants.SaveAsync(cfg).ConfigureAwait(
false);
463 StatusMessage =
"✅ 현재 CodeMaru/Dreamine 계정이 이 포트폴리오의 관리자로 추가되었습니다.";
465 else if (
string.IsNullOrWhiteSpace(cfg.OwnerUserId))
467 StatusMessage =
"✅ 로그인 성공. 공용 계정에 연결하려면 먼저 CodeMaru/Dreamine 로그인을 해주세요.";
504 if (
Config ==
null)
return;
510 foreach (var p
in Projects.Where(p =>
511 !
string.IsNullOrWhiteSpace(p.ImageFileName) && p.WorkImages.Length == 0))
512 p.WorkImages = [p.ImageFileName!];
602 EditingProject.WorkImages = [..
EditingProject.WorkImages, saved];
604 EditingProject.ImageFileName = saved;
610 EditingProject.WorkImages = [..
EditingProject.WorkImages, saved];
611 EditingProject.ImageFileName ??= saved;
618 EditingProject.VideoFileNames = [..
EditingProject.VideoFileNames, saved];
670 await
_projects.DeleteAsync(slug, projectId);
717 await
_media.DeleteAsync(slug, projectId, fileName);
719 EditingProject.WorkImages =
EditingProject.WorkImages.Where(f => f != fileName).ToArray();
758 if (!fn.StartsWith(
'/'))
759 await
_media.DeleteAsync(slug, projectId, fn);
760 EditingProject.ImageFileName =
null;
805 await
_media.DeleteAsync(slug, projectId, fileName);
807 EditingProject.VideoFileNames =
EditingProject.VideoFileNames.Where(f => f != fileName).ToArray();
850 public string GetMediaUrl(
string slug,
string projectId,
string fileName) =>
851 _media.GetMediaUrl(slug, projectId, fileName);
893 public string GetImageUrl(
string slug,
string projectId,
string fileName) =>
936 public string GetVideoUrl(
string slug,
string projectId,
string fileName) =>
964 url.StartsWith(
'/') || url.StartsWith(
"http://") || url.StartsWith(
"https://");
999 _media.GetProfileImageUrl(slug, fileName);
1043 var skills =
NewSkillNames.Split(
',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
1222 if (
Config ==
null)
return;
1223 Config.PasswordHash = DreaminePasswordHasher.HashPlainTextForStorage(
Config.PasswordHash);
1255 if (
Config is
null)
return;
1256 var user = await
_userContext.GetCurrentAsync().ConfigureAwait(
false);
1263 if (
string.Equals(
Config.OwnerUserId, userId, StringComparison.Ordinal))
1269 var removed =
GetAdminUsers(
Config).RemoveAll(x =>
string.Equals(x.UserId, userId, StringComparison.Ordinal));
1332 if (!DreaminePasswordHasher.VerifyPassword(current,
Config.PasswordHash)) {
StatusMessage =
"❌ 현재 비밀번호가 틀렸습니다.";
return false; }
1333 if (next.Length < 8) {
StatusMessage =
"❌ 새 비밀번호는 8자 이상이어야 합니다.";
return false; }
1334 if (next != confirm) {
StatusMessage =
"❌ 새 비밀번호가 일치하지 않습니다.";
return false; }
1335 Config.PasswordHash = DreaminePasswordHasher.HashPassword(next);
1375 if (
Config ==
null)
return;
1377 var saved = await
_media.SaveProfileImageAsync(slug, file);
1378 Config.ProfileImageFileName = saved;
1417 await
_contacts.MarkReadAsync(slug, msgId);
1455 await
_contacts.DeleteAsync(slug, msgId);
1509 var user = await
_userContext.GetCurrentAsync().ConfigureAwait(
false);
1512 ?
string.IsNullOrWhiteSpace(user.DisplayName)
1514 : $
"{user.DisplayName} ({user.Provider})"
1577 user.IsAuthenticated &&
1579 string.Equals(config.
OwnerUserId, user.Id, StringComparison.Ordinal);
1615 if (!user.IsAuthenticated)
return false;
1617 return GetAdminUsers(config).Any(x =>
string.Equals(x.UserId, user.Id, StringComparison.Ordinal));
1654 if (!user.IsAuthenticated)
return;
1657 var existing = users.FirstOrDefault(x =>
string.Equals(x.UserId, user.Id, StringComparison.Ordinal));
1658 if (existing is
null)
1663 Provider = user.Provider,
1665 DisplayName = user.DisplayName,
1667 AddedAt = DateTime.Now
1672 existing.Provider = user.Provider;
1673 existing.Email = user.Email;
1674 existing.DisplayName = user.DisplayName;
1675 if (role ==
"Owner")
1677 existing.Role =
"Owner";
1708 if (!
string.IsNullOrWhiteSpace(config.
OwnerUserId) &&
1709 users.All(x => !
string.Equals(x.UserId, config.
OwnerUserId, StringComparison.Ordinal)))
1718 AddedAt = config.OwnerLinkedAt ?? config.CreatedAt
1722 foreach (var user
in users.Where(x =>
string.Equals(x.UserId, config.
OwnerUserId, StringComparison.Ordinal)))
1724 user.Role =
"Owner";
1728 .OrderByDescending(x =>
string.Equals(x.Role,
"Owner", StringComparison.OrdinalIgnoreCase))
1729 .ThenBy(x => x.AddedAt)
1742internal static class ProjectItemExtensions
record PortfolioCurrentUser(string Id, string Provider, string Email, string DisplayName)
string GetVideoUrl(string slug, string projectId, string fileName)
void RemoveEducation(EducationItem e)
ExperienceItem? EditingExp
PortfolioAdminViewModel(IPortfolioTenantStore tenants, IProjectStore projects, IResumeStore resumes, IContactStore contacts, IMediaService media, PortfolioUserContext userContext)
bool IsLinkedToCurrentUser
string GetProfileImageUrl(string slug, string fileName)
void EditExperience(ExperienceItem e)
async Task SaveProjectAsync(string slug)
async Task DeleteProjectAsync(string slug, string projectId)
static IReadOnlyList< PortfolioAdminUser > BuildEffectiveAdminUsers(PortfolioConfig config)
static void EnsureAdminUser(PortfolioConfig config, PortfolioCurrentUser user, string role)
readonly IPortfolioTenantStore _tenants
IReadOnlyList< PortfolioAdminUser > EffectiveAdminUsers
static bool IsOwnerUser(PortfolioConfig config, PortfolioCurrentUser user)
async Task InitializeAsync(string slug)
List< IBrowserFile > PendingVideos
void EditProject(ProjectItem p)
async Task LoadAsync(string slug)
async Task RemoveAdminAsync(string userId)
List< ContactMessage > Messages
void EditEducation(EducationItem e)
async Task DeleteProjectVideoAsync(string slug, string projectId, string fileName)
async Task SaveResumeAsync(string slug)
readonly IProjectStore _projects
static List< PortfolioAdminUser > GetAdminUsers(PortfolioConfig config)
async Task DeleteMessageAsync(string slug, string msgId)
async Task MarkReadAsync(string slug, string msgId)
readonly IResumeStore _resumes
readonly IContactStore _contacts
List< ProjectItem > Projects
static bool IsExternalUrl(string url)
async Task RefreshCurrentUserAsync()
List< IBrowserFile > PendingFiles
readonly IMediaService _media
readonly PortfolioUserContext _userContext
async Task UploadProfileImageAsync(string slug, IBrowserFile file)
async Task< bool > ChangePasswordAsync(string slug, string current, string next, string confirm)
async Task SaveConfigAsync()
async Task DeleteProjectCoverAsync(string slug, string projectId)
void RemoveSkillGroup(SkillGroup g)
async Task< bool > LoginAsync(string slug)
EducationItem? EditingEdu
async Task DeleteSelfAsync(string slug)
string GetImageUrl(string slug, string projectId, string fileName)
async Task DeleteProjectMediaAsync(string slug, string projectId, string fileName)
ProjectItem? EditingProject
string GetMediaUrl(string slug, string projectId, string fileName)
void RemoveExperience(ExperienceItem e)
static bool IsAdminUser(PortfolioConfig config, PortfolioCurrentUser user)