Game.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. using System.ComponentModel.DataAnnotations;
  2. namespace Domain.Entities.Store;
  3. /// <summary>
  4. /// 게임사 제휴 마스터. 상품/쿠폰의 필수 FK.
  5. /// CommissionRate(게임사 몫) % 만 정의. 채널 보상률은 Channel.StoreCommissionRate 에서 채널별로 동적으로 결정.
  6. /// dpot 몫 = 100 - Game.CommissionRate (채널 미선택) 또는 100 - Game.CommissionRate - Channel.StoreCommissionRate (채널 선택).
  7. /// </summary>
  8. public class Game
  9. {
  10. [Key]
  11. public int ID { get; private set; }
  12. /// <summary>게임 코드 (예: "LOL", "BG3"). UNIQUE. 외부 연동/내부 식별자.</summary>
  13. public string? Code { get; private set; }
  14. /// <summary>게임 한글명 (필수, UNIQUE) — 예: "리그 오브 레전드"</summary>
  15. public string KorName { get; private set; } = default!;
  16. /// <summary>게임 영문명 (선택) — 예: "League of Legends"</summary>
  17. public string? EngName { get; private set; }
  18. /// <summary>게임사/퍼블리셔 (예: "Riot Games Korea")</summary>
  19. public string Publisher { get; private set; } = default!;
  20. /// <summary>작은 이미지 (목록 카드/썸네일용)</summary>
  21. public string? Thumbnail { get; private set; }
  22. /// <summary>큰 이미지 (상품 상세 페이지 배너, 상점 메인 배너용)</summary>
  23. public string? BigImage { get; private set; }
  24. /// <summary>게임 소개 (HTML — CKEditor)</summary>
  25. public string? Description { get; private set; }
  26. /// <summary>게임사 수익률(커미션) % — 0~100. 주문 금액의 이 비율을 게임사 정산 큐에 적재.</summary>
  27. public decimal CommissionRate { get; private set; }
  28. /// <summary>게임 내 결제 보고(공개 API /v1/purchases) 채널 수수료율 % — 0~100. 0 이면 해당 게임 결제 등록 거부.</summary>
  29. public decimal ApiCommissionRate { get; private set; }
  30. /// <summary>게임 다운로드 또는 공식 사이트 주소 (선택)</summary>
  31. public string? Link { get; private set; }
  32. /// <summary>YouTube 예고편 URL (선택). 렌더 시 embed 로 정규화.</summary>
  33. public string? TrailerUrl { get; private set; }
  34. /// <summary>최소 사양 (자유 텍스트).</summary>
  35. public string? MinSpec { get; private set; }
  36. /// <summary>권장 사양 (자유 텍스트).</summary>
  37. public string? RecommendedSpec { get; private set; }
  38. /// <summary>게임 출시일 (선택).</summary>
  39. public DateOnly? ReleaseDate { get; private set; }
  40. public virtual List<GameImage> GameImage { get; private set; } = [];
  41. public virtual List<GameGenreMap> GameGenreMap { get; private set; } = [];
  42. public virtual List<GameCategoryMap> GameCategoryMap { get; private set; } = [];
  43. public virtual List<GameLanguageSupport> GameLanguageSupport { get; private set; } = [];
  44. public bool IsActive { get; private set; } = true;
  45. public int Order { get; private set; }
  46. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  47. public DateTime? UpdatedAt { get; private set; }
  48. private Game() { }
  49. public static Game Create(
  50. string korName,
  51. string publisher,
  52. decimal commissionRate,
  53. string? code = null,
  54. string? engName = null,
  55. string? thumbnail = null,
  56. string? bigImage = null,
  57. string? description = null,
  58. string? link = null,
  59. int order = 0
  60. ) {
  61. ValidateRate(commissionRate);
  62. ValidateNames(korName, engName);
  63. ValidatePublisher(publisher);
  64. ValidateCode(code);
  65. ValidateLink(link);
  66. return new Game
  67. {
  68. Code = NormalizeOptional(code),
  69. KorName = korName.Trim(),
  70. EngName = NormalizeOptional(engName),
  71. Publisher = publisher.Trim(),
  72. CommissionRate = commissionRate,
  73. Thumbnail = thumbnail,
  74. BigImage = bigImage,
  75. Description = description,
  76. Link = NormalizeOptional(link),
  77. Order = order
  78. };
  79. }
  80. public void Update(
  81. string korName,
  82. string publisher,
  83. decimal commissionRate,
  84. string? code,
  85. string? engName,
  86. string? thumbnail,
  87. string? bigImage,
  88. string? description,
  89. string? link,
  90. int order,
  91. bool isActive
  92. ) {
  93. ValidateRate(commissionRate);
  94. ValidateNames(korName, engName);
  95. ValidatePublisher(publisher);
  96. ValidateCode(code);
  97. ValidateLink(link);
  98. Code = NormalizeOptional(code);
  99. KorName = korName.Trim();
  100. EngName = NormalizeOptional(engName);
  101. Publisher = publisher.Trim();
  102. CommissionRate = commissionRate;
  103. Thumbnail = thumbnail;
  104. BigImage = bigImage;
  105. Description = description;
  106. Link = NormalizeOptional(link);
  107. Order = order;
  108. IsActive = isActive;
  109. UpdatedAt = DateTime.UtcNow;
  110. }
  111. public void Deactivate()
  112. {
  113. IsActive = false;
  114. UpdatedAt = DateTime.UtcNow;
  115. }
  116. /// <summary>API 결제 보고 채널 수수료율 설정. Create/Update 시그니처 변경 없이 별도 관리 (Admin 폼 전용).</summary>
  117. public void SetApiCommissionRate(decimal rate)
  118. {
  119. ValidateRate(rate);
  120. ApiCommissionRate = rate;
  121. UpdatedAt = DateTime.UtcNow;
  122. }
  123. /// <summary>상세 미디어/사양 설정 (Admin 게임 상세 폼 전용). Create/Update 시그니처 변경 회피.</summary>
  124. public void SetDetail(string? trailerUrl, string? minSpec, string? recommendedSpec, DateOnly? releaseDate)
  125. {
  126. if (trailerUrl is not null && trailerUrl.Length > 500)
  127. {
  128. throw new ArgumentOutOfRangeException(nameof(trailerUrl), "TrailerUrl 은 500자 이하로 입력하세요.");
  129. }
  130. TrailerUrl = NormalizeOptional(trailerUrl);
  131. MinSpec = NormalizeOptional(minSpec);
  132. RecommendedSpec = NormalizeOptional(recommendedSpec);
  133. ReleaseDate = releaseDate;
  134. UpdatedAt = DateTime.UtcNow;
  135. }
  136. private static void ValidateRate(decimal commissionRate)
  137. {
  138. if (commissionRate < 0 || commissionRate > 100)
  139. {
  140. throw new ArgumentOutOfRangeException(nameof(commissionRate), "0~100 사이의 값이어야 합니다.");
  141. }
  142. }
  143. private static void ValidateNames(string korName, string? engName)
  144. {
  145. if (string.IsNullOrWhiteSpace(korName))
  146. {
  147. throw new ArgumentException("KorName is required.", nameof(korName));
  148. }
  149. if (korName.Length > 255)
  150. {
  151. throw new ArgumentOutOfRangeException(nameof(korName), "KorName 은 255자 이하로 입력하세요.");
  152. }
  153. if (engName is not null && engName.Length > 255)
  154. {
  155. throw new ArgumentOutOfRangeException(nameof(engName), "EngName 은 255자 이하로 입력하세요.");
  156. }
  157. }
  158. private static void ValidatePublisher(string publisher)
  159. {
  160. if (string.IsNullOrWhiteSpace(publisher))
  161. {
  162. throw new ArgumentException("Publisher is required.", nameof(publisher));
  163. }
  164. if (publisher.Length > 100)
  165. {
  166. throw new ArgumentOutOfRangeException(nameof(publisher), "Publisher 는 100자 이하로 입력하세요.");
  167. }
  168. }
  169. private static void ValidateCode(string? code)
  170. {
  171. if (code is not null && code.Length > 6)
  172. {
  173. throw new ArgumentOutOfRangeException(nameof(code), "Code 는 6자 이하로 입력하세요.");
  174. }
  175. }
  176. private static void ValidateLink(string? link)
  177. {
  178. if (link is not null && link.Length > 255)
  179. {
  180. throw new ArgumentOutOfRangeException(nameof(link), "Link 는 255자 이하로 입력하세요.");
  181. }
  182. }
  183. private static string? NormalizeOptional(string? value)
  184. {
  185. if (string.IsNullOrWhiteSpace(value))
  186. {
  187. return null;
  188. }
  189. return value.Trim();
  190. }
  191. }