using System.ComponentModel.DataAnnotations; namespace Domain.Entities.Store; /// /// 게임사 제휴 마스터. 상품/쿠폰의 필수 FK. /// CommissionRate(게임사 몫) % 만 정의. 채널 보상률은 Channel.StoreCommissionRate 에서 채널별로 동적으로 결정. /// antooza 몫 = 100 - Game.CommissionRate (채널 미선택) 또는 100 - Game.CommissionRate - Channel.StoreCommissionRate (채널 선택). /// public class Game { [Key] public int ID { get; private set; } /// 게임 코드 (예: "LOL", "BG3"). UNIQUE. 외부 연동/내부 식별자. public string? Code { get; private set; } /// 게임 한글명 (필수, UNIQUE) — 예: "리그 오브 레전드" public string KorName { get; private set; } = default!; /// 게임 영문명 (선택) — 예: "League of Legends" public string? EngName { get; private set; } /// 게임사/퍼블리셔 (예: "Riot Games Korea") public string Publisher { get; private set; } = default!; /// 작은 이미지 (목록 카드/썸네일용) public string? Thumbnail { get; private set; } /// 큰 이미지 (상품 상세 페이지 배너, 상점 메인 배너용) public string? BigImage { get; private set; } /// 게임 소개 (HTML — CKEditor) public string? Description { get; private set; } /// 게임사 수익률(커미션) % — 0~100. 주문 금액의 이 비율을 게임사 정산 큐에 적재. public decimal CommissionRate { get; private set; } /// 게임 다운로드 또는 공식 사이트 주소 (선택) public string? Link { get; private set; } /// YouTube 예고편 URL (선택). 렌더 시 embed 로 정규화. public string? TrailerUrl { get; private set; } /// 최소 사양 (자유 텍스트). public string? MinSpec { get; private set; } /// 권장 사양 (자유 텍스트). public string? RecommendedSpec { get; private set; } /// 게임 출시일 (선택). public DateOnly? ReleaseDate { get; private set; } public virtual List GameImage { get; private set; } = []; public virtual List GameGenreMap { get; private set; } = []; public virtual List GameCategoryMap { get; private set; } = []; public virtual List GameLanguageSupport { get; private set; } = []; public bool IsActive { get; private set; } = true; public int Order { get; private set; } public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; private set; } private Game() { } public static Game Create( string korName, string publisher, decimal commissionRate, string? code = null, string? engName = null, string? thumbnail = null, string? bigImage = null, string? description = null, string? link = null, int order = 0 ) { ValidateRate(commissionRate); ValidateNames(korName, engName); ValidatePublisher(publisher); ValidateCode(code); ValidateLink(link); return new Game { Code = NormalizeOptional(code), KorName = korName.Trim(), EngName = NormalizeOptional(engName), Publisher = publisher.Trim(), CommissionRate = commissionRate, Thumbnail = thumbnail, BigImage = bigImage, Description = description, Link = NormalizeOptional(link), Order = order }; } public void Update( string korName, string publisher, decimal commissionRate, string? code, string? engName, string? thumbnail, string? bigImage, string? description, string? link, int order, bool isActive ) { ValidateRate(commissionRate); ValidateNames(korName, engName); ValidatePublisher(publisher); ValidateCode(code); ValidateLink(link); Code = NormalizeOptional(code); KorName = korName.Trim(); EngName = NormalizeOptional(engName); Publisher = publisher.Trim(); CommissionRate = commissionRate; Thumbnail = thumbnail; BigImage = bigImage; Description = description; Link = NormalizeOptional(link); Order = order; IsActive = isActive; UpdatedAt = DateTime.UtcNow; } public void Deactivate() { IsActive = false; UpdatedAt = DateTime.UtcNow; } /// 상세 미디어/사양 설정 (Admin 게임 상세 폼 전용). Create/Update 시그니처 변경 회피. public void SetDetail(string? trailerUrl, string? minSpec, string? recommendedSpec, DateOnly? releaseDate) { if (trailerUrl is not null && trailerUrl.Length > 500) { throw new ArgumentOutOfRangeException(nameof(trailerUrl), "TrailerUrl 은 500자 이하로 입력하세요."); } TrailerUrl = NormalizeOptional(trailerUrl); MinSpec = NormalizeOptional(minSpec); RecommendedSpec = NormalizeOptional(recommendedSpec); ReleaseDate = releaseDate; UpdatedAt = DateTime.UtcNow; } private static void ValidateRate(decimal commissionRate) { if (commissionRate < 0 || commissionRate > 100) { throw new ArgumentOutOfRangeException(nameof(commissionRate), "0~100 사이의 값이어야 합니다."); } } private static void ValidateNames(string korName, string? engName) { if (string.IsNullOrWhiteSpace(korName)) { throw new ArgumentException("KorName is required.", nameof(korName)); } if (korName.Length > 255) { throw new ArgumentOutOfRangeException(nameof(korName), "KorName 은 255자 이하로 입력하세요."); } if (engName is not null && engName.Length > 255) { throw new ArgumentOutOfRangeException(nameof(engName), "EngName 은 255자 이하로 입력하세요."); } } private static void ValidatePublisher(string publisher) { if (string.IsNullOrWhiteSpace(publisher)) { throw new ArgumentException("Publisher is required.", nameof(publisher)); } if (publisher.Length > 100) { throw new ArgumentOutOfRangeException(nameof(publisher), "Publisher 는 100자 이하로 입력하세요."); } } private static void ValidateCode(string? code) { if (code is not null && code.Length > 6) { throw new ArgumentOutOfRangeException(nameof(code), "Code 는 6자 이하로 입력하세요."); } } private static void ValidateLink(string? link) { if (link is not null && link.Length > 255) { throw new ArgumentOutOfRangeException(nameof(link), "Link 는 255자 이하로 입력하세요."); } } private static string? NormalizeOptional(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return value.Trim(); } }