using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Domain.Entities.Members; namespace Domain.Entities.Forum.Posts; /// /// 분석글 응원(Cheer) — D3 M4 (Twitch Bits 모델). /// 후원자(From)가 글 작성자(To)에게 캐시/토큰을 팁으로 보내며 플랫폼이 수수료를 취한다. /// 순액(NetAmount = Amount − FeeAmount)만 작성자 지갑 Reward 파티션으로 적립되고(출금 불가), /// 수수료(FeeAmount)는 어디에도 적립되지 않고 소각(=유통에서 제거)된다. /// /// 규제 가드라인(§⑧): 응원은 사후·자율·무대가이며 글 열람 조건이 아니다(글은 항상 무료). /// 본인 글 응원 금지·일일 발신 캡·수신 적립분 출금 불가는 자전 수수료 소각 어뷰징을 차단한다. /// public class PostCheer { [ForeignKey(nameof(PostID))] public virtual Post Post { get; private set; } = null!; [ForeignKey(nameof(FromMemberID))] public virtual Member? FromMember { get; private set; } [ForeignKey(nameof(ToMemberID))] public virtual Member? ToMember { get; private set; } [Key] public int ID { get; private set; } /// 응원 대상 게시글 public int PostID { get; private set; } /// 응원 발신자 (후원자) public int FromMemberID { get; private set; } /// 응원 수신자 (글 작성자 스냅샷) public int ToMemberID { get; private set; } /// 총 응원 금액 (발신자 차감액) public int Amount { get; private set; } /// 플랫폼 수수료 (소각분, 어디에도 적립되지 않음) public int FeeAmount { get; private set; } /// 작성자 수령 순액 (Amount − FeeAmount, Reward 파티션 적립) public int NetAmount { get; private set; } /// 응원 한마디 (선택) public string? Message { get; private set; } public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; private PostCheer() { } private PostCheer(int postID, int fromMemberID, int toMemberID, int amount, int feeAmount, int netAmount, string? message) { PostID = postID; FromMemberID = fromMemberID; ToMemberID = toMemberID; Amount = amount; FeeAmount = feeAmount; NetAmount = netAmount; Message = message; } public static PostCheer Create(int postID, int fromMemberID, int toMemberID, int amount, int feeAmount, int netAmount, string? message) { if (postID <= 0) { throw new ArgumentOutOfRangeException(nameof(postID)); } if (fromMemberID <= 0) { throw new ArgumentOutOfRangeException(nameof(fromMemberID)); } if (toMemberID <= 0) { throw new ArgumentOutOfRangeException(nameof(toMemberID)); } if (fromMemberID == toMemberID) { throw new ArgumentException("본인 글에는 응원할 수 없습니다.", nameof(toMemberID)); } if (amount <= 0) { throw new ArgumentOutOfRangeException(nameof(amount)); } if (feeAmount < 0 || netAmount <= 0 || feeAmount + netAmount != amount) { throw new ArgumentException("수수료/순액 계산이 올바르지 않습니다.", nameof(netAmount)); } return new PostCheer(postID, fromMemberID, toMemberID, amount, feeAmount, netAmount, message); } }