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; }
///
/// YouTube API 로 채널 데이터를 마지막으로 갱신한 시각(UTC).
/// YouTube API Services ToS 의 30일 데이터 갱신·삭제 정책 enforcement 용.
/// null 이거나 UtcNow - 30d 이전이면 WipeYouTubeData() 대상.
///
public DateTime? YouTubeLastSyncedAt { get; private set; }
/// 후원(Donation) 수수료(%) — antooza 가 가져가는 비율. PlatformFeeRate=20% 이면 채널주는 80% 보유.
public decimal PlatformFeeRate { get; private set; } = 0;
/// 상점 매출 보상률(%) — 채널이 가져가는 비율. StoreCommissionRate=20% 이면 채널주에게 20% 입금, antooza가 80% 보유.
public decimal StoreCommissionRate { get; private set; } = 0;
public bool IsVerified { get; private set; } = false;
public bool IsActive { get; private set; } = true;
/// 채널 후원 코드(4~7자 영문+숫자, 대문자 저장). 한 번 발급되면 변경 불가. 후원 채널 빠른 검색·게임 in-app 결제 식별용.
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;
}
///
/// YouTube API Services ToS 의 30일 데이터 갱신·삭제 정책에 따라
/// 마지막 동기화 시점이 30일을 초과한 경우 YouTube 측 데이터를 영구 폐기한다.
/// 채널 자체는 유지하되, YouTube 출처 필드만 null·0 처리.
/// 호출 후 LastSyncedAt 도 초기화하여 재갱신 시점까지 wipe 상태 유지.
///
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;
}
///
/// 핸들 저장 canonical form 정규화: 앞의 '@' 접두사 제거 (YouTube customUrl은 '@foo' 형태로 오지만 DB에는 'foo'로 저장)
///
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;
}
///
/// Admin 전용 후원 코드 설정/변경/해제.
/// 일반 사용자(Studio) 의 IssueDonationCode 와 달리 변경 제약이 없다.
/// null/공백 → 코드 해제(NULL). 값 있으면 4~7자 영문+숫자 정규식 검증 후 대문자로 저장.
///
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;
}
///
/// 후원 코드 1회성 발급. 이미 발급된 경우 InvalidOperationException.
/// 형식: 4~7자 영문+숫자 (대문자로 정규화 저장).
///
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");
}
}