| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- using System.Text.RegularExpressions;
- namespace Domain.Entities.Members;
- public class Channel
- {
- [ForeignKey(nameof(MemberID))]
- public virtual Member Member { get; private set; } = null!;
- [Key]
- public int ID { get; private set; }
- public int MemberID { get; private set; }
- public string SID { get; private set; } = default!;
- public string Name { get; private set; } = default!;
- public string? Handle { get; private set; }
- public string YouTubeUrl { get; private set; } = default!;
- public string? YouTubeChannelID { get; private set; }
- public string? Description { get; private set; }
- public string? ThumbnailUrl { get; private set; }
- public string? BannerUrl { get; private set; }
- public long SubscriberCount { get; private set; }
- public long VideoCount { get; private set; }
- public long ViewCount { get; private set; }
- public string? Email { get; private set; }
- public DateTime? YouTubePublishedAt { get; private set; }
- /// <summary>
- /// YouTube API 로 채널 데이터를 마지막으로 갱신한 시각(UTC).
- /// YouTube API Services ToS 의 30일 데이터 갱신·삭제 정책 enforcement 용.
- /// null 이거나 UtcNow - 30d 이전이면 WipeYouTubeData() 대상.
- /// </summary>
- public DateTime? YouTubeLastSyncedAt { get; private set; }
- /// <summary>후원(Donation) 수수료(%) — dpot 이 가져가는 비율. PlatformFeeRate=20% 이면 채널주는 80% 보유.</summary>
- public decimal PlatformFeeRate { get; private set; } = 0;
- /// <summary>상점 매출 보상률(%) — 채널이 가져가는 비율. StoreCommissionRate=20% 이면 채널주에게 20% 입금, dpot이 80% 보유.</summary>
- public decimal StoreCommissionRate { get; private set; } = 0;
- public bool IsVerified { get; private set; } = false;
- public bool IsActive { get; private set; } = true;
- /// <summary>채널 후원 코드(4~7자 영문+숫자, 대문자 저장). 한 번 발급되면 변경 불가. 후원 채널 빠른 검색·게임 in-app 결제 식별용.</summary>
- public string? DonationCode { get; private set; }
- public string WidgetToken { get; private set; } = GenerateWidgetToken();
- public DateTime? UpdatedAt { get; private set; }
- public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
- private Channel() { }
- private Channel(int memberID, string sid, string name, string youtubeUrl)
- {
- if (memberID <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(memberID));
- }
- if (string.IsNullOrWhiteSpace(sid))
- {
- throw new ArgumentException("SID is required.", nameof(sid));
- }
- if (sid.Length != 24)
- {
- throw new ArgumentOutOfRangeException(nameof(sid));
- }
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("Name is required.", nameof(name));
- }
- if (name.Length > 200)
- {
- throw new ArgumentOutOfRangeException(nameof(name));
- }
- if (string.IsNullOrWhiteSpace(youtubeUrl))
- {
- throw new ArgumentException("YouTubeUrl is required.", nameof(youtubeUrl));
- }
- if (youtubeUrl.Length > 255)
- {
- throw new ArgumentOutOfRangeException(nameof(youtubeUrl));
- }
- MemberID = memberID;
- SID = sid;
- Name = name;
- YouTubeUrl = youtubeUrl;
- }
- public static Channel Create(int memberID, string sid, string name, string youtubeUrl)
- {
- return new(memberID, sid, name, youtubeUrl);
- }
- public void Update(string name, string? handle, string youtubeUrl, decimal platformFeeRate, decimal storeCommissionRate, bool isVerified, bool isActive)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("Name is required.", nameof(name));
- }
- if (name.Length > 200)
- {
- throw new ArgumentOutOfRangeException(nameof(name));
- }
- var normalizedHandle = NormalizeHandleInternal(handle);
- if (normalizedHandle is not null && normalizedHandle.Length > 30)
- {
- throw new ArgumentOutOfRangeException(nameof(handle));
- }
- if (string.IsNullOrWhiteSpace(youtubeUrl))
- {
- throw new ArgumentException("YouTubeUrl is required.", nameof(youtubeUrl));
- }
- if (youtubeUrl.Length > 255)
- {
- throw new ArgumentOutOfRangeException(nameof(youtubeUrl));
- }
- if (platformFeeRate < 0 || platformFeeRate > 100)
- {
- throw new ArgumentOutOfRangeException(nameof(platformFeeRate));
- }
- if (storeCommissionRate < 0 || storeCommissionRate > 100)
- {
- throw new ArgumentOutOfRangeException(nameof(storeCommissionRate));
- }
- Name = name;
- Handle = normalizedHandle;
- YouTubeUrl = youtubeUrl;
- PlatformFeeRate = platformFeeRate;
- StoreCommissionRate = storeCommissionRate;
- IsVerified = isVerified;
- IsActive = isActive;
- UpdatedAt = DateTime.UtcNow;
- }
- public void UpdateHandle(string? handle)
- {
- var normalizedHandle = NormalizeHandleInternal(handle);
- if (normalizedHandle is not null && normalizedHandle.Length > 30)
- {
- throw new ArgumentOutOfRangeException(nameof(handle));
- }
- Handle = normalizedHandle;
- UpdatedAt = DateTime.UtcNow;
- }
- public void UpdateYouTubeInfo(
- string youtubeChannelID,
- string name,
- string? handle,
- string? description,
- string? thumbnailUrl,
- string? bannerUrl,
- long subscriberCount,
- long videoCount,
- long viewCount,
- string? email,
- DateTime? youtubePublishedAt
- ) {
- YouTubeChannelID = youtubeChannelID;
- Name = name;
- Handle = NormalizeHandleInternal(handle);
- Description = description;
- ThumbnailUrl = thumbnailUrl;
- BannerUrl = bannerUrl;
- SubscriberCount = subscriberCount;
- VideoCount = videoCount;
- ViewCount = viewCount;
- Email = email;
- IsActive = true;
- YouTubePublishedAt = youtubePublishedAt;
- YouTubeLastSyncedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// YouTube API Services ToS 의 30일 데이터 갱신·삭제 정책에 따라
- /// 마지막 동기화 시점이 30일을 초과한 경우 YouTube 측 데이터를 영구 폐기한다.
- /// 채널 자체는 유지하되, YouTube 출처 필드만 null·0 처리.
- /// 호출 후 LastSyncedAt 도 초기화하여 재갱신 시점까지 wipe 상태 유지.
- /// </summary>
- public void WipeYouTubeData()
- {
- YouTubeChannelID = null;
- Description = null;
- ThumbnailUrl = null;
- BannerUrl = null;
- SubscriberCount = 0;
- VideoCount = 0;
- ViewCount = 0;
- Email = null;
- YouTubePublishedAt = null;
- YouTubeLastSyncedAt = null;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// 핸들 저장 canonical form 정규화: 앞의 '@' 접두사 제거 (YouTube customUrl은 '@foo' 형태로 오지만 DB에는 'foo'로 저장)
- /// </summary>
- private static string? NormalizeHandleInternal(string? handle)
- {
- if (string.IsNullOrWhiteSpace(handle))
- {
- return null;
- }
- var trimmed = handle.TrimStart('@');
- return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;
- }
- public void Deactivate()
- {
- IsActive = false;
- UpdatedAt = DateTime.UtcNow;
- }
- public void ResetWidgetToken()
- {
- WidgetToken = GenerateWidgetToken();
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// Admin 전용 후원 코드 설정/변경/해제.
- /// 일반 사용자(Studio) 의 IssueDonationCode 와 달리 변경 제약이 없다.
- /// null/공백 → 코드 해제(NULL). 값 있으면 4~7자 영문+숫자 정규식 검증 후 대문자로 저장.
- /// </summary>
- public void SetDonationCodeByAdmin(string? code)
- {
- if (string.IsNullOrWhiteSpace(code))
- {
- DonationCode = null;
- UpdatedAt = DateTime.UtcNow;
- return;
- }
- var trimmed = code.Trim();
- if (!Regex.IsMatch(trimmed, "^[A-Za-z0-9]{4,7}$"))
- {
- throw new ArgumentException("DonationCode must be 4~7 alphanumeric characters.", nameof(code));
- }
- DonationCode = trimmed.ToUpperInvariant();
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// 후원 코드 1회성 발급. 이미 발급된 경우 InvalidOperationException.
- /// 형식: 4~7자 영문+숫자 (대문자로 정규화 저장).
- /// </summary>
- public void IssueDonationCode(string code)
- {
- if (DonationCode is not null)
- {
- throw new InvalidOperationException("DonationCode already issued.");
- }
- if (string.IsNullOrEmpty(code))
- {
- throw new ArgumentException("DonationCode is required.", nameof(code));
- }
- if (!Regex.IsMatch(code, "^[A-Za-z0-9]{4,7}$"))
- {
- throw new ArgumentException("DonationCode must be 4~7 alphanumeric characters.", nameof(code));
- }
- DonationCode = code.ToUpperInvariant();
- UpdatedAt = DateTime.UtcNow;
- }
- private static string GenerateWidgetToken()
- {
- return Guid.NewGuid().ToString("N");
- }
- }
|