KIM-JINO5 vor 4 Monaten
Ursprung
Commit
7caccaa486
49 geänderte Dateien mit 3212 neuen und 1 gelöschten Zeilen
  1. 0 1
      Application/Application.csproj
  2. 98 0
      Domain/Entities/Forum/Boards/Board.cs
  3. 67 0
      Domain/Entities/Forum/Boards/BoardGroup.cs
  4. 55 0
      Domain/Entities/Forum/Boards/BoardManager.cs
  5. 712 0
      Domain/Entities/Forum/Boards/BoardMeta.cs
  6. 62 0
      Domain/Entities/Forum/Boards/BoardPrefix.cs
  7. 166 0
      Domain/Entities/Forum/Comments/Comment.cs
  8. 84 0
      Domain/Entities/Forum/Comments/CommentFile.cs
  9. 87 0
      Domain/Entities/Forum/Comments/CommentImage.cs
  10. 61 0
      Domain/Entities/Forum/Comments/CommentLink.cs
  11. 54 0
      Domain/Entities/Forum/Comments/CommentMedia.cs
  12. 62 0
      Domain/Entities/Forum/Comments/CommentMention.cs
  13. 69 0
      Domain/Entities/Forum/Comments/CommentReaction.cs
  14. 84 0
      Domain/Entities/Forum/Comments/CommentReport.cs
  15. 22 0
      Domain/Entities/Forum/Constants/BoardConstant.cs
  16. 52 0
      Domain/Entities/Forum/Constants/CommentConstant.cs
  17. 52 0
      Domain/Entities/Forum/Constants/PostConstant.cs
  18. 51 0
      Domain/Entities/Forum/Logs/CommentFileDownLog.cs
  19. 51 0
      Domain/Entities/Forum/Logs/CommentLinkClickLog.cs
  20. 54 0
      Domain/Entities/Forum/Logs/CommentUpdateLog.cs
  21. 60 0
      Domain/Entities/Forum/Logs/PostFileDownLog.cs
  22. 60 0
      Domain/Entities/Forum/Logs/PostLinkClickLog.cs
  23. 72 0
      Domain/Entities/Forum/Logs/PostUpdateLog.cs
  24. 181 0
      Domain/Entities/Forum/Posts/Post.cs
  25. 62 0
      Domain/Entities/Forum/Posts/PostBookmark.cs
  26. 96 0
      Domain/Entities/Forum/Posts/PostFile.cs
  27. 99 0
      Domain/Entities/Forum/Posts/PostImage.cs
  28. 66 0
      Domain/Entities/Forum/Posts/PostLink.cs
  29. 57 0
      Domain/Entities/Forum/Posts/PostMedia.cs
  30. 71 0
      Domain/Entities/Forum/Posts/PostReaction.cs
  31. 87 0
      Domain/Entities/Forum/Posts/PostReport.cs
  32. 47 0
      Domain/Entities/Forum/Posts/PostTag.cs
  33. 47 0
      Domain/Entities/Forum/Posts/Tag.cs
  34. 18 0
      Domain/Entities/Forum/ValueObject/BoardLayout.cs
  35. 18 0
      Domain/Entities/Forum/ValueObject/BoardMetaType.cs
  36. 25 0
      Domain/Entities/Forum/ValueObject/BoardNotify.cs
  37. 18 0
      Domain/Entities/Forum/ValueObject/BoardPermission.cs
  38. 21 0
      Domain/Entities/Forum/ValueObject/BoardSearch.cs
  39. 21 0
      Domain/Entities/Forum/ValueObject/BoardSort.cs
  40. 27 0
      Domain/Entities/Forum/ValueObject/BoardType.cs
  41. 15 0
      Domain/Entities/Forum/ValueObject/CommentSort.cs
  42. 30 0
      Domain/Entities/Forum/ValueObject/DisplayStatus.cs
  43. 15 0
      Domain/Entities/Forum/ValueObject/Reaction.cs
  44. 18 0
      Domain/Entities/Forum/ValueObject/ReportStatus.cs
  45. 38 0
      Domain/Entities/Forum/ValueObject/ReportType.cs
  46. 0 0
      Infrastructure/Persistence/Migrations/20260205091913_a3.Designer.cs
  47. 0 0
      Infrastructure/Persistence/Migrations/20260205091913_a3.cs
  48. 0 0
      Infrastructure/Persistence/Migrations/20260206143102_a4.Designer.cs
  49. 0 0
      Infrastructure/Persistence/Migrations/20260206143102_a4.cs

+ 0 - 1
Application/Application.csproj

@@ -9,7 +9,6 @@
   <ItemGroup>
     <Folder Include="Authentication\" />
     <Folder Include="Behaviors\" />
-    <Folder Include="Features\Member\Log\" />
     <Folder Include="Features\ReferenceData\Dtos\" />
   </ItemGroup>
 

+ 98 - 0
Domain/Entities/Forum/Boards/Board.cs

@@ -0,0 +1,98 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace Domain.Entities.Forum.Boards
+{
+    [Table(nameof(Board))]
+    [Index(nameof(BoardGroupID))]
+    [Index(nameof(Code), IsUnique = true)]
+    [Index(nameof(Name))]
+    [Index(nameof(Order))]
+    [Index(nameof(IsSearch))]
+    [Index(nameof(IsActive))]
+    [Index(nameof(Posts))]
+    [Index(nameof(Comments))]
+    [Index(nameof(IsSearch), nameof(IsActive))]
+    [Index(nameof(Code), nameof(IsSearch))]
+    [Index(nameof(Code), nameof(IsActive))]
+    [Index(nameof(Code), nameof(IsSearch))]
+    public class Board
+    {
+        // 게시판
+        [ForeignKey(nameof(BoardGroupID))]
+        public virtual BoardGroup BoardGroup { get; set; } = null!;
+
+        // 게시판 속성
+        public virtual BoardMeta BoardMeta { get; set; } = null!;
+
+        // 게시판 관리자
+        public virtual List<BoardManager> BoardManager { get; set; } = [];
+
+        // 말머리
+        public virtual List<BoardPrefix> BoardPrefix { get; set; } = [];
+
+        // 게시글
+        public virtual List<Post> Post { get; set; } = [];
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("분류 ID")]
+        [Comment("분류 ID")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public int BoardGroupID { get; set; }
+
+        [DisplayName("게시판 주소")]
+        [Comment("게시판 주소")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [StringLength(30, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Code { get; set; } = null!;
+
+        [DisplayName("게시판 이름")]
+        [Comment("게시판 이름")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [StringLength(70, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Name { get; set; } = null!;
+
+        [DisplayName("순서")]
+        [Comment("순서")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [Range(short.MinValue, short.MaxValue, ErrorMessage = "{0}는 {2} ~ {1} 사이를 입력합니다.")]
+        public short Order { get; set; } = 0;
+
+        [DisplayName("검색 여부")]
+        [Comment("검색 여부")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public bool IsSearch { get; set; } = false;
+
+        [DisplayName("사용 여부")]
+        [Comment("사용 여부")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public bool IsActive { get; set; } = false;
+
+        [DisplayName("게시글 수")]
+        [Comment("게시글 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0}은 {2} 이상, {1} 이하로 입력하세요.")]
+        public int Posts { get; set; } = 0;
+
+        [DisplayName("댓글 수")]
+        [Comment("댓글 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0}은 {2} 이상, {1} 이하로 입력하세요.")]
+        public int Comments { get; set; } = 0;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 67 - 0
Domain/Entities/Forum/Boards/BoardGroup.cs

@@ -0,0 +1,67 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace Domain.Entities.Forum.Boards
+{
+    [Table(nameof(BoardGroup))]
+    [Index(nameof(Code), IsUnique = true)]
+    [Index(nameof(Order))]
+    [Index(nameof(Order), nameof(CreatedAt))]
+    [Index(nameof(Code), nameof(Order), nameof(CreatedAt))]
+    public class BoardGroup
+    {
+        public virtual List<Board> Board { get; set; } = [];
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 분류 주소")]
+        [Comment("게시판 분류 주소")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [DataType(DataType.Text)]
+        [StringLength(30, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Code { get; set; } = null!;
+
+        [DisplayName("게시판 분류 명")]
+        [Comment("게시판 분류 명")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [StringLength(70, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Name { get; set; } = null!;
+
+        [DisplayName("순서")]
+        [Comment("순서")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [Range(short.MinValue, short.MaxValue, ErrorMessage = "{0} 허용 범위는 {2} ~ {1} 입니다.")]
+        public short Order { get; set; } = 0;
+
+        [DisplayName("게시판 수")]
+        [Comment("게시판 수")]
+        [Range(0, short.MaxValue, ErrorMessage = "{0} 허용 범위는 {2} ~ {1} 입니다.")]
+        public short Boards { get; set; } = 0;
+
+        [DisplayName("게시글 수")]
+        [Comment("게시글 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0} 허용 범위는 {2} ~ {1} 입니다.")]
+        public int Posts { get; set; } = 0;
+
+        [DisplayName("댓글 수")]
+        [Comment("댓글 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0} 허용 범위는 {2} ~ {1} 입니다.")]
+        public int Comments { get; set; } = 0;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 55 - 0
Domain/Entities/Forum/Boards/BoardManager.cs

@@ -0,0 +1,55 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Domain.Entities.Members;
+
+namespace Domain.Entities.Forum.Boards
+{
+    [Table(nameof(BoardManager))]
+    [Index(nameof(BoardID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(BoardID), nameof(MemberID))]
+    public class BoardManager
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public virtual Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public int BoardID { get; set; }
+
+        [DisplayName("관리자 ID")]
+        [Comment("관리자 ID")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public string MemberID { get; set; } = default!;
+
+        [DisplayName("수정 권한")]
+        [Comment("수정 권한")]
+        public bool CanEdit { get; set; } = false;
+
+        [DisplayName("삭제 권한")]
+        [Comment("삭제 권한")]
+        public bool CanDelete { get; set; } = false;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; }
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 712 - 0
Domain/Entities/Forum/Boards/BoardMeta.cs

@@ -0,0 +1,712 @@
+using Domain.Entities.Forum.Constants;
+using Domain.Entities.Forum.ValueObject;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace Domain.Entities.Forum.Boards
+{
+    [Table(nameof(BoardMeta))]
+    [Index(nameof(BoardID), IsUnique = true)]
+    public class BoardMeta
+    {
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public int BoardID { get; set; }
+
+        // 목록
+        public BoardMetaList List { get; set; } = new BoardMetaList();
+
+        // 열람
+        public BoardMetaView View { get; set; } = new BoardMetaView();
+
+        // 작성
+        public BoardMetaWrite Write { get; set; } = new BoardMetaWrite();
+
+        // 댓글
+        public BoardMetaComment Comment { get; set; } = new BoardMetaComment();
+
+        // 일반
+        public BoardMetaGeneral General { get; set; } = new BoardMetaGeneral();
+
+        // 권한
+        public BoardMetaPermission Permission { get; set; } = new BoardMetaPermission();
+
+        // 알람
+        public BoardMetaNotify Notify { get; set; } = new BoardMetaNotify();
+
+        // 알람 양식
+        public BoardMetaNotifyTemplate NotifyTemplate { get; set; } = new BoardMetaNotifyTemplate();
+
+        // 경험치
+        public BoardMetaExp Exp { get; set; } = new BoardMetaExp();
+    }
+
+    /// <summary>
+    /// 게시판 목록 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaList
+    {
+        [DisplayName("상단 내용 출력 여부")]
+        [Comment("상단 내용 출력 여부")]
+        public bool ShowHeader { get; set; } = false;
+
+        [DisplayName("상단 내용")]
+        [Comment("상단 내용")]
+        public string? HeaderContent { get; set; } = null;
+
+        [DisplayName("하단 내용 출력 여부")]
+        [Comment("하단 내용 출력 여부")]
+        public bool ShowFooter { get; set; } = false;
+
+        [DisplayName("하단 내용")]
+        [Comment("하단 내용")]
+        public string? FooterContent { get; set; } = null;
+
+        [DisplayName("게시판 종류")]
+        [Comment("게시판 종류")]
+        public BoardLayout? Layout { get; set; }
+
+        [DisplayName("기본 정렬")]
+        [Comment("기본 정렬")]
+        public BoardSort? Sort { get; set; }
+
+        [DisplayName("목록 표시")]
+        [Comment("목록 표시")]
+        [Range(0, 100, ErrorMessage = "값은 0 ~ 100 사이를 입력합니다.")]
+        public byte PerPage { get; set; } = 0;
+
+        [DisplayName("글쓰기 버튼 보이기")]
+        [Comment("글쓰기 버튼 보이기")]
+        public bool AlwaysShowWriteButton { get; set; } = false;
+
+        [DisplayName("하단 목록 보이기")]
+        [Comment("하단 목록 보이기")]
+        public bool ShowFooterListView { get; set; } = false;
+
+        [DisplayName("NEW 사용 여부")]
+        [Comment("NEW 사용 여부")]
+        public bool IsNewIcon { get; set; } = false;
+
+        [DisplayName("HOT 사용 여부")]
+        [Comment("HOT 사용 여부")]
+        public bool IsHotIcon { get; set; } = false;
+
+        [DisplayName("공지사항 제외 여부")]
+        [Comment("공지사항 제외 여부")]
+        public bool ExceptNotice { get; set; } = false;
+
+        [DisplayName("전체공지 제외 여부")]
+        [Comment("전체공지 제외 여부")]
+        public bool ExceptSpeaker { get; set; } = false;
+    }
+
+    /// <summary>
+    /// 게시판 열람 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaView
+    {
+        [DisplayName("즐겨찾기 기능")]
+        [Comment("즐겨찾기 기능")]
+        public bool AllowBookmark { get; set; } = false;
+
+        [DisplayName("공감 기능")]
+        [Comment("공감 기능")]
+        public bool AllowLike { get; set; } = false;
+
+        [DisplayName("비공감 기능")]
+        [Comment("비공감 기능")]
+        public bool AllowDislike { get; set; } = false;
+
+        [DisplayName("본문 인쇄 기능")]
+        [Comment("본문 인쇄 기능")]
+        public bool AllowPrint { get; set; } = false;
+
+        [DisplayName("SNS 보내기 기능")]
+        [Comment("SNS 보내기 기능")]
+        public bool AllowSnsShare { get; set; } = false;
+
+        [DisplayName("이전글, 다음글 버튼")]
+        [Comment("이전글, 다음글 버튼")]
+        public bool AllowPrevNextBotton { get; set; } = false;
+
+        [DisplayName("신고 기능")]
+        [Comment("신고 기능")]
+        public bool AllowBlame { get; set; } = false;
+
+        [DisplayName("신고 시 숨김")]
+        [Comment("신고 시 숨김")]
+        public ushort BlameHideCount { get; set; } = 0;
+
+        [DisplayName("URL Link 새창")]
+        [Comment("URL Link 새창")]
+        public bool AllowContentLinkTargetBlank { get; set; } = false;
+
+        [DisplayName("주소 복사 버튼")]
+        [Comment("주소 복사 버튼")]
+        public bool AllowPostUrlCopy { get; set; } = false;
+
+        [DisplayName("글 주소 QR 코드")]
+        [Comment("글 주소 QR 코드")]
+        public bool AllowPostUrlQrCode { get; set; } = false;
+
+        [DisplayName("회원 사진 공개")]
+        [Comment("회원 사진 공개")]
+        public bool ShowMemberPhoto { get; set; } = false;
+
+        [DisplayName("회원 아이콘 공개")]
+        [Comment("회원 아이콘 공개")]
+        public bool ShowMemberIcon { get; set; } = false;
+
+        [DisplayName("회원 가입일 공개")]
+        [Comment("회원 가입일 공개")]
+        public bool ShowMemberRegDate { get; set; } = false;
+
+        [DisplayName("오늘의 한마디 공개")]
+        [Comment("오늘의 한마디 공개")]
+        public bool ShowMemberSummary { get; set; } = false;
+    }
+
+    /// <summary>
+    /// 게시판 작성 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaWrite
+    {
+        [DisplayName("상단 내용 출력 여부")]
+        [Comment("상단 내용 출력 여부")]
+        public bool ShowHeader { get; set; } = false;
+
+        [DisplayName("상단 내용")]
+        [Comment("상단 내용")]
+        public string? HeaderContent { get; set; } = null;
+
+        [DisplayName("하단 내용 출력 여부")]
+        [Comment("하단 내용 출력 여부")]
+        public bool ShowFooter { get; set; } = false;
+
+        [DisplayName("하단 내용")]
+        [Comment("하단 내용")]
+        public string? FooterContent { get; set; } = null;
+
+        [DisplayName("기본 제목")]
+        [Comment("기본 제목")]
+        [StringLength(PostConstant.MaxAllowedSubjectLength, ErrorMessage = "{0}은(는) {1}자 이내로 입력해주세요.")]
+        public string? DefaultSubject { get; set; } = null;
+
+        [DisplayName("기본 내용")]
+        [Comment("기본 내용")]
+        [StringLength(PostConstant.MaxAllowedContentLength, ErrorMessage = "{0}은(는) {1}자 이내로 입력해주세요.")]
+        public string? DefaultContent { get; set; } = null;
+
+        [DisplayName("웹 에디터 사용")]
+        [Comment("웹 에디터 사용")]
+        public bool AllowEditor { get; set; } = false;
+
+        [DisplayName("말머리 사용")]
+        [Comment("말머리 사용")]
+        public bool AllowPrefix { get; set; } = false;
+
+        [DisplayName("말머리 필수 선택")]
+        [Comment("말머리 필수 선택")]
+        public bool RequiredPrefix { get; set; } = false;
+
+        [DisplayName("비밀글 사용")]
+        [Comment("비밀글 사용")]
+        public bool AllowSecret { get; set; } = false;
+
+        [DisplayName("태그 사용")]
+        [Comment("태그 사용")]
+        public bool AllowTag { get; set; } = false;
+
+        [DisplayName("태그 개수 제한")]
+        [Comment("태그 개수 제한")]
+        [Range(0, PostConstant.MaxAllowedTags, ErrorMessage = "{0}은(는) {1} ~ {2} 사이여야 합니다.")]
+        public byte TagLimit { get; set; } = PostConstant.MaxAllowedTags;
+
+        [DisplayName("이미지 사용")]
+        [Comment("이미지 사용")]
+        public bool AllowImage { get; set; } = false;
+
+        [DisplayName("이미지 개수 제한")]
+        [Comment("이미지 개수 제한")]
+        [Range(0, PostConstant.MaxAllowedImages, ErrorMessage = "{0}은(는) {1} ~ {2} 사이여야 합니다.")]
+        public byte ImageUploadLimit { get; set; } = PostConstant.MaxAllowedImages;
+
+        [DisplayName("이미지 용량 제한(KB)")]
+        [Comment("이미지 용량 제한(KB)")]
+        [Range(0, PostConstant.MaxAllowedImageSize, ErrorMessage = "{0}은(는) {1} ~ {2} 사이여야 합니다.")]
+        public int ImageUploadMaxSize { get; set; } = PostConstant.MaxAllowedImageSize;
+
+        [DisplayName("미디어 사용")]
+        [Comment("미디어 사용")]
+        public bool AllowMedia { get; set; } = false;
+
+        [DisplayName("미디어 개수 제한")]
+        [Comment("미디어 개수 제한")]
+        [Range(0, PostConstant.MaxAllowedMedias, ErrorMessage = "{0}은(는) {1} ~ {2} 사이여야 합니다.")]
+        public byte MediaUploadLimit { get; set; } = PostConstant.MaxAllowedMedias;
+
+        [DisplayName("파일 사용")]
+        [Comment("파일 사용")]
+        public bool AllowFile { get; set; } = false;
+
+        [DisplayName("파일 개수 제한")]
+        [Comment("파일 개수 제한")]
+        [Range(0, PostConstant.MaxAllowedFiles, ErrorMessage = "{0}은(는) {1} ~ {2} 사이여야 합니다.")]
+        public byte FileUploadLimit { get; set; } = PostConstant.MaxAllowedFiles;
+
+        [DisplayName("파일 용량 제한(KB)")]
+        [Comment("파일 용량 제한(KB)")]
+        [Range(0, PostConstant.MaxAllowedFileSize, ErrorMessage = "{0}은(는) {1} ~ {2} 사이여야 합니다.")]
+        public int FileUploadMaxSize { get; set; } = PostConstant.MaxAllowedFileSize;
+
+        [DisplayName("파일 허용 확장자")]
+        [Comment("파일 허용 확장자")]
+        [StringLength(200, ErrorMessage = "{0}은(는) {1}자 이내로 입력해주세요.")]
+        public string? FileUploadExtension { get; set; } = null;
+    }
+
+    /// <summary>
+    /// 게시판 일반 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaGeneral
+    {
+        [DisplayName("게시글 삭제 금지 기간")]
+        [Comment("게시글 삭제 금지 기간")]
+        [Range(0, 365, ErrorMessage = "{0}은(는) 0 ~ 365 사이를 입력합니다.")]
+        public ushort DeleteProtectionDays { get; set; } = 0;
+
+        [DisplayName("게시글 수정 금지 기간")]
+        [Comment("게시글 수정 금지 기간")]
+        [Range(0, 365, ErrorMessage = "{0}은(는) 0 ~ 365 사이를 입력합니다.")]
+        public ushort UpdateProtectionDays { get; set; } = 0;
+
+        [DisplayName("게시글 보호 기능 (삭제 시)")]
+        [Comment("게시글 보호 기능 (삭제 시)")]
+        public bool AllowDeleteProtection { get; set; } = false;
+
+        [DisplayName("게시글 보호 기능 (수정 시)")]
+        [Comment("게시글 보호 기능 (수정 시)")]
+        public bool AllowUpdateProtection { get; set; } = false;
+
+        [DisplayName("다운로드 기록")]
+        [Comment("다운로드 기록")]
+        public bool EnableFileDownLog { get; set; } = false;
+
+        [DisplayName("링크 클릭 기록")]
+        [Comment("링크 클릭 기록")]
+        public bool EnableLinkClickLog { get; set; } = false;
+
+        [DisplayName("게시글 변경 기록")]
+        [Comment("게시글 변경 기록")]
+        public bool EnablePostUpdateLog { get; set; } = false;
+    }
+
+    /// <summary>
+    /// 게시판 권한 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaPermission
+    {
+        [DisplayName("게시판 접근")]
+        [Comment("게시판 접근")]
+        public short BoardAccess { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("글 열람")]
+        [Comment("글 열람")]
+        public short PostView { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("글 작성")]
+        [Comment("글 작성")]
+        public short PostWrite { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("댓글 목록")]
+        [Comment("댓글 목록")]
+        public short CommentView { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("댓글 작성")]
+        [Comment("댓글 작성")]
+        public short CommentWrite { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("답글 작성")]
+        [Comment("답글 작성")]
+        public short ReplyWrite { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("파일 업로드")]
+        [Comment("파일 업로드")]
+        public short FileUpload { get; set; } = (short)BoardPermission.Guest;
+
+        [DisplayName("파일 다운로드")]
+        [Comment("파일 다운로드")]
+        public short FileDownload { get; set; } = (short)BoardPermission.Guest;
+    }
+
+    /// <summary>
+    /// 게시판 알림 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaNotify
+    {
+        [DisplayName("게시글 작성 시")]
+        [Comment("게시글 작성 시")]
+        public byte? PostWriteNotify { get; set; } = null;
+
+        [DisplayName("댓글 작성 시")]
+        [Comment("댓글 작성 시")]
+        public byte? CommentWriteNotify { get; set; } = null;
+
+        [DisplayName("답글 작성 시")]
+        [Comment("답글 작성 시")]
+        public byte? ReplyWriteNotify { get; set; } = null;
+
+        public BoardNotify PostWriteNotifyEnum => (BoardNotify)(PostWriteNotify ?? 0);
+        public BoardNotify CommentWriteNotifyEnum => (BoardNotify)(CommentWriteNotify ?? 0);
+        public BoardNotify ReplyWriteNotifyEnum => (BoardNotify)(ReplyWriteNotify ?? 0);
+    }
+
+    /// <summary>
+    /// 게시판 경험치 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaExp
+    {
+        [DisplayName("경험치 기능")]
+        [Comment("경험치 기능")]
+        public bool EnableExp { get; set; } = false;
+
+        [DisplayName("경험치 안내")]
+        [Comment("경험치 안내")]
+        public bool ShowExpGuide { get; set; } = false;
+
+        /*
+         * 경험치 지급량 조절
+         */
+        [DisplayName("게시글 작성")]
+        [Comment("게시글 작성")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort PostWriteExp { get; set; } = 0;
+
+        [DisplayName("댓글 작성")]
+        [Comment("댓글 작성")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort CommentWriteExp { get; set; } = 0;
+
+        [DisplayName("파일 업로드")]
+        [Comment("파일 업로드")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort FileUploadExp { get; set; } = 0;
+
+        [DisplayName("파일 다운로드")]
+        [Comment("파일 다운로드")]
+        [Range(-10000, 10000, ErrorMessage = "값은 -10,000 ~ 10,000 사이를 입력합니다.")]
+        public short FileDownloadExp { get; set; } = 0;
+
+        [DisplayName("게시글 읽기")]
+        [Comment("게시글 읽기")]
+        [Range(-10000, 10000, ErrorMessage = "값은 -10,000 ~ 10,000 사이를 입력합니다.")]
+        public short OtherPostReadExp { get; set; } = 0;
+
+        [DisplayName("게시글 좋아요")]
+        [Comment("게시글 좋아요")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherPostLikeExp { get; set; } = 0;
+
+        [DisplayName("게시글 싫어요")]
+        [Comment("게시글 싫어요")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherPostDisLikeExp { get; set; } = 0;
+
+        [DisplayName("댓글 좋아요")]
+        [Comment("댓글 좋아요")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherCommentLikeExp { get; set; } = 0;
+
+        [DisplayName("댓글 싫어요")]
+        [Comment("댓글 싫어요")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherCommentDisLikeExp { get; set; } = 0;
+
+        [DisplayName("내 게시글 읽힘")]
+        [Comment("내 게시글 읽힘")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnPostReadExp { get; set; } = 0;
+
+        [DisplayName("내 게시글 좋아요")]
+        [Comment("내 게시글 좋아요")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnPostLikeExp { get; set; } = 0;
+
+        [DisplayName("내 게시글 싫어요")]
+        [Comment("내 게시글 싫어요")]
+        [Range(-10000, 10000, ErrorMessage = "값은 -10,000 ~ 10,000 사이를 입력합니다.")]
+        public short OwnPostDisLikeExp { get; set; } = 0;
+
+        [DisplayName("내 댓글 좋아요")]
+        [Comment("내 댓글 좋아요")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnCommentLikeExp { get; set; } = 0;
+
+        [DisplayName("내 댓글 싫어요")]
+        [Comment("내 댓글 싫어요")]
+        [Range(-10000, 10000, ErrorMessage = "값은 -10,000 ~ 10,000 사이를 입력합니다.")]
+        public short OwnCommentDisLikeExp { get; set; } = 0;
+
+        /*
+         * 경험치 회수량 조절 (항상 음수 계산)
+         */
+        [DisplayName("게시글 작성 취소")]
+        [Comment("게시글 작성 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort PostWriteUndoExp { get; set; } = 0;
+
+        [DisplayName("댓글 작성 취소")]
+        [Comment("댓글 작성 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort CommentWriteUndoExp { get; set; } = 0;
+
+        [DisplayName("파일 업로드 취소")]
+        [Comment("파일 업로드 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort FileUploadUndoExp { get; set; } = 0;
+
+        [DisplayName("게시글 읽기 취소")]
+        [Comment("게시글 읽기 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherPostReadUndoExp { get; set; } = 0;
+
+        [DisplayName("게시글 좋아요 취소")]
+        [Comment("게시글 좋아요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherPostLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("게시글 싫어요 취소")]
+        [Comment("게시글 싫어요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherPostDisLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("댓글 좋아요 취소")]
+        [Comment("댓글 좋아요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherCommentLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("댓글 싫어요 취소")]
+        [Comment("댓글 싫어요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OtherCommentDisLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("내 게시글 읽힘 취소")]
+        [Comment("내 게시글 읽힘 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnPostReadUndoExp { get; set; } = 0;
+
+        [DisplayName("내 게시글 좋아요 취소")]
+        [Comment("내 게시글 좋아요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnPostLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("내 게시글 싫어요 취소")]
+        [Comment("내 게시글 싫어요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnPostDisLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("내 댓글 좋아요 취소")]
+        [Comment("내 댓글 좋아요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnCommentLikeUndoExp { get; set; } = 0;
+
+        [DisplayName("내 댓글 싫어요 취소")]
+        [Comment("내 댓글 싫어요 취소")]
+        [Range(0, 10000, ErrorMessage = "값은 0 ~ 10,000 사이를 입력합니다.")]
+        public ushort OwnCommentDisLikeUndoExp { get; set; } = 0;
+
+        /*
+         * 경험치 지급 기한
+         */
+        [DisplayName("게시글 작성 기한")]
+        [Comment("게시글 작성 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort PostWriteExpWithinDays { get; set; } = 0;
+
+        [DisplayName("댓글 작성 기한")]
+        [Comment("댓글 작성 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort CommentWriteExpWithinDays { get; set; } = 0;
+
+        [DisplayName("파일 업로드 기한")]
+        [Comment("파일 업로드 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort FileUploadExpWithinDays { get; set; } = 0;
+
+        [DisplayName("게시글 읽기 기한")]
+        [Comment("게시글 읽기 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OtherPostReadExpWithinDays { get; set; } = 0;
+
+        [DisplayName("게시글 좋아요 기한")]
+        [Comment("게시글 좋아요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OtherPostLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("게시글 싫어요 기한")]
+        [Comment("게시글 싫어요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OtherPostDisLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("댓글 좋아요 기한")]
+        [Comment("댓글 좋아요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OtherCommentLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("댓글 싫어요 기한")]
+        [Comment("댓글 싫어요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OtherCommentDisLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("내 게시글 읽힘 기한")]
+        [Comment("내 게시글 읽힘 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OwnPostReadExpWithinDays { get; set; } = 0;
+
+        [DisplayName("내 게시글 좋아요 기한")]
+        [Comment("내 게시글 좋아요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OwnPostLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("내 게시글 싫어요 기한")]
+        [Comment("내 게시글 싫어요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OwnPostDisLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("내 댓글 좋아요 기한")]
+        [Comment("내 댓글 좋아요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OwnCommentLikeExpWithinDays { get; set; } = 0;
+
+        [DisplayName("내 댓글 싫어요 기한")]
+        [Comment("내 댓글 싫어요 기한")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public ushort OwnCommentDisLikeExpWithinDays { get; set; } = 0;
+    }
+
+    /// <summary>
+    /// 게시판 알림 양식
+    /// </summary>
+    [Owned]
+    public class BoardMetaNotifyTemplate
+    {
+        [DisplayName("이메일 제목 - 게시글 작성 시")]
+        [Comment("이메일 제목 - 게시글 작성 시")]
+        public string? PostWriteEmailNotifySubject { get; set; } = null;
+
+        [DisplayName("이메일 내용 - 게시글 작성 시")]
+        [Comment("이메일 내용 - 게시글 작성 시")]
+        public string? PostWriteEmailNotifyContent { get; set; } = null;
+
+        [DisplayName("이메일 제목 - 댓글 작성 시")]
+        [Comment("이메일 제목 - 댓글 작성 시")]
+        public string? CommentWriteEmailNotifySubject { get; set; } = null;
+
+        [DisplayName("이메일 내용 - 댓글 작성 시")]
+        [Comment("이메일 내용 - 댓글 작성 시")]
+        public string? CommentWriteEmailNotifyContent { get; set; } = null;
+
+        [DisplayName("이메일 제목 - 답글 작성 시")]
+        [Comment("이메일 제목 - 답글 작성 시")]
+        public string? ReplyWriteEmailNotifySubject { get; set; } = null;
+
+        [DisplayName("이메일 내용 - 답글 작성 시")]
+        [Comment("이메일 내용 - 답글 작성 시")]
+        public string? ReplyWriteEmailNotifyContent { get; set; } = null;
+    }
+
+    /// <summary>
+    /// 게시판 댓글 설정
+    /// </summary>
+    [Owned]
+    public class BoardMetaComment
+    {
+        [DisplayName("댓글 사용")]
+        [Comment("댓글 사용")]
+        public bool EnableComment { get; set; } = false;
+
+        [DisplayName("목록 표시")]
+        [Comment("목록 표시")]
+        [Range(0, 100, ErrorMessage = "값은 0 ~ 100 사이를 입력합니다.")]
+        public ushort PerPage { get; set; } = 0;
+
+        [DisplayName("댓글 공감 사용")]
+        [Comment("댓글 공감 사용")]
+        public bool AllowLike { get; set; } = false;
+
+        [DisplayName("댓글 비공감 사용")]
+        [Comment("댓글 비공감 사용")]
+        public bool AllowDisLike { get; set; } = false;
+
+        [DisplayName("회원 사진 공개")]
+        [Comment("회원 사진 공개")]
+        public bool ShowMemberPhoto { get; set; } = false;
+
+        [DisplayName("회원 아이콘 공개")]
+        [Comment("회원 아이콘 공개")]
+        public bool ShowMemberIcon { get; set; } = false;
+
+        [DisplayName("안내 문구")]
+        [Comment("안내 문구")]
+        [StringLength(1000)]
+        public string? ContentPlaceholder { get; set; } = null;
+
+        [DisplayName("최소 입력 글자")]
+        [Comment("최소 입력 글자")]
+        public ushort MinContentLength { get; set; } = 0;
+
+        [DisplayName("최대 입력 글자")]
+        [Comment("최대 입력 글자")]
+        public ushort MaxContentLength { get; set; } = 0;
+
+        [DisplayName("웹 에디터 사용")]
+        [Comment("웹 에디터 사용")]
+        public bool EnableEditor { get; set; } = false;
+
+        [DisplayName("비밀글 사용")]
+        [Comment("비밀글 사용")]
+        public bool AllowSecret { get; set; } = false;
+
+        [DisplayName("댓글 신고 시 숨김")]
+        [Comment("댓글 신고 시 숨김")]
+        public ushort BlameHideCount { get; set; } = 0;
+
+        [DisplayName("댓글 삭제 금지 기간")]
+        [Comment("댓글 삭제 금지 기간")]
+        public ushort DeleteProtectionDays { get; set; } = 0;
+
+        [DisplayName("댓글 수정 금지 기간")]
+        [Comment("댓글 수정 금지 기간")]
+        public ushort UpdateProtectionDays { get; set; } = 0;
+
+        [DisplayName("댓글 보호 기능 (삭제 시)")]
+        [Comment("댓글 보호 기능 (삭제 시)")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public bool AllowDeleteProtection { get; set; } = false;
+
+        [DisplayName("댓글 보호 기능 (수정 시)")]
+        [Comment("댓글 보호 기능 (수정 시)")]
+        [Range(0, 365, ErrorMessage = "값은 0 ~ 365 사이를 입력합니다.")]
+        public bool AllowUpdateProtection { get; set; } = false;
+
+        [DisplayName("댓글 변경 기록")]
+        [Comment("댓글 변경 기록")]
+        public bool EnableCommentUpdateLog { get; set; } = false;
+    }
+}

+ 62 - 0
Domain/Entities/Forum/Boards/BoardPrefix.cs

@@ -0,0 +1,62 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace Domain.Entities.Forum.Boards
+{
+    [Table(nameof(BoardPrefix))]
+    [Index(nameof(BoardID), nameof(IsActive), nameof(Order), nameof(CreatedAt))]
+    public class BoardPrefix
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        public int BoardID { get; set; }
+
+        [DisplayName("말머리")]
+        [Comment("말머리")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [StringLength(20, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Name { get; set; } = null!;
+
+        [DisplayName("색상")]
+        [Comment("색상")]
+        [StringLength(10, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? Color { get; set; } = null;
+
+        [DisplayName("정렬 순서")]
+        [Comment("정렬 순서")]
+        [Required(ErrorMessage = "{0}는 필수입니다.")]
+        [Range(short.MinValue, short.MaxValue, ErrorMessage = "{0}는 {2} ~ {1} 사이를 입력합니다.")]
+        public short Order { get; set; } = 0;
+
+        [DisplayName("사용 게시글 수")]
+        [Comment("사용 게시글 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0}은 {2}자 이상, {1}자 이하로 입력해 주세요.")]
+        public int Posts { get; set; } = 0;
+
+        [DisplayName("사용 여부")]
+        [Comment("사용 여부")]
+        public bool IsActive { get; set; } = false;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 166 - 0
Domain/Entities/Forum/Comments/Comment.cs

@@ -0,0 +1,166 @@
+using Domain.Entities.Forum.ValueObject;
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table(nameof(Comment))]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(ParentID))]
+
+    // 루트 댓글 페이징 & 정렬용
+    [Index(nameof(PostID), nameof(IsDeleted), nameof(ParentID), nameof(CreatedAt), IsDescending = new[] { false, false, false, true })]
+    [Index(nameof(PostID), nameof(IsDeleted), nameof(ParentID), nameof(Score), nameof(ID), IsDescending = new[] { false, false, false, true, true })]
+    [Index(nameof(PostID), nameof(MemberID), nameof(IsDeleted), nameof(ParentID), nameof(CreatedAt), IsDescending = new[] { false, false, false, false, true })]
+    [Index(nameof(PostID), nameof(MemberID), nameof(IsDeleted), nameof(ParentID), nameof(Score), nameof(ID), IsDescending = new[] { false, false, false, false, true, true })]
+
+    // 자식 댓글(답글) 등록순
+    [Index(nameof(PostID), nameof(IsDeleted), nameof(ParentID), nameof(CreatedAt))]
+    [Index(nameof(PostID), nameof(MemberID), nameof(IsDeleted), nameof(ParentID), nameof(CreatedAt))]
+    public class Comment
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public virtual Member Member { get; set; } = null!;
+
+        [ForeignKey(nameof(MentionMemberID))]
+        public virtual Member? MentionMember { get; set; }
+
+        public virtual Comment? Parent { get; set; }
+        public virtual ICollection<Comment> Children { get; set; } = [];
+
+        // 댓글 이미지
+        public virtual List<CommentImage> CommentImage { get; set; } = [];
+
+        // 댓글 미디어
+        public virtual List<CommentMedia> CommentMedia { get; set; } = [];
+
+        // 댓글 파일
+        public virtual List<CommentFile> CommentFile { get; set; } = [];
+
+        // 댓글 링크
+        public virtual List<CommentLink> CommentLink { get; set; } = [];
+
+        // 댓글 좋아요/싫어요
+        public virtual List<CommentReaction> CommentReaction { get; set; } = [];
+
+        // 댓글 신고
+        public virtual List<CommentReport> CommentReport { get; set; } = [];
+
+        // 댓글 파일 다운로드 기록
+        public virtual List<CommentFileDownLog> CommentFileDownLog { get; set; } = [];
+
+        // 댓글 링크 클릭 기록
+        public virtual List<CommentLinkClickLog> CommentLinkClickLog { get; set; } = [];
+
+        // 댓글 변경 기록
+        public virtual List<CommentUpdateLog> CommentUpdateLog { get; set; } = [];
+
+        // 답글 언급
+        public virtual CommentMention? CommentMention { get; set; }
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [Comment("부모 댓글 ID")]
+        public int? ParentID { get; set; }
+
+        [Comment("언급 대상 회원 ID")]
+        public int? MentionMemberID { get; set; }
+
+        [Comment("댓글 깊이")]
+        public sbyte Depth { get; set; } = 0;
+
+        [Comment("댓글 내용")]
+        [StringLength(4000, MinimumLength = 1)]
+        public string Content { get; set; } = default!;
+
+        [Comment("회원 SID")]
+        [StringLength(20, MinimumLength = 1)]
+        public string SID { get; set; } = default!;
+
+        [Comment("회원 이메일")]
+        [StringLength(60, MinimumLength = 1)]
+        public string Email { get; set; } = default!;
+
+        [Comment("회원 이름")]
+        [StringLength(20)]
+        public string? Name { get; set; }
+
+        [Comment("1:1문의 답변 여부")]
+        public bool IsReply { get; set; } = false;
+
+        [Comment("비밀글 여부")]
+        public bool IsSecret { get; set; } = false;
+
+        [Comment("삭제 여부")]
+        public bool IsDeleted { get; set; } = false;
+
+        [Comment("좋아요")]
+        public int Likes { get; set; } = 0;
+
+        [Comment("싫어요")]
+        public int Dislikes { get; set; } = 0;
+
+        [Comment("신고 수")]
+        public int Reports { get; set; } = 0;
+
+        [Comment("답글 수")]
+        public int Replies { get; set; } = 0;
+
+        [Comment("인기 순위")]
+        public int Score { get; set; } = 0; // 정렬을 위한
+
+        [Comment("이미지 수")]
+        public int Images { get; set; } = 0;
+
+        [Comment("미디어 수")]
+        public byte Medias { get; set; } = 0;
+
+        [Comment("파일 수")]
+        public byte Files { get; set; } = 0;
+
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("상태")]
+        [EnumDataType(typeof(DisplayStatus), ErrorMessage = "올바른 상태를 입력해주세요.")]
+        public DisplayStatus Status { get; set; } = DisplayStatus.Normal;
+
+        [Comment("삭제 일시")]
+        public DateTime? DeletedAt { get; set; }
+
+        [Comment("수정 일시")]
+        public DateTime? UpdatedAt { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 84 - 0
Domain/Entities/Forum/Comments/CommentFile.cs

@@ -0,0 +1,84 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentFile")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(UUID), IsUnique = true)]
+    [Index(nameof(CommentID), nameof(HashedName))]
+    [Index(nameof(CommentID), nameof(HashedName), nameof(IsDisabled))]
+    [Index(nameof(CommentID), nameof(HashedName), nameof(IsDisabled), nameof(ID))]
+    public class CommentFile
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = default!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = default!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = default!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("파일 ID")]
+        public Guid UUID { get; set; }
+
+        [Comment("원본 파일명")]
+        [StringLength(255)]
+        public string FileName { get; set; } = null!;
+
+        [Comment("저장 파일명")]
+        [StringLength(255)]
+        public string HashedName { get; set; } = null!;
+
+        [Comment("저장 경로")]
+        [StringLength(500)]
+        public string Path { get; set; } = null!;
+
+        [Comment("URL")]
+        [StringLength(1000)]
+        public string Url { get; set; } = null!;
+
+        [Comment("확장자")]
+        [StringLength(10)]
+        public string? Extension { get; set; }
+
+        [Comment("MIME Type")]
+        [StringLength(100)]
+        public string? ContentType { get; set; }
+
+        [Comment("용량(byte)")]
+        public long? Size { get; set; } = 0;
+
+        [Comment("다운로드 수")]
+        public int Downloads { get; set; } = 0;
+
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [Comment("비활성 일시")]
+        public DateTime? DisabledAt { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 87 - 0
Domain/Entities/Forum/Comments/CommentImage.cs

@@ -0,0 +1,87 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentImage")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(UUID), IsUnique = true)]
+    [Index(nameof(CommentID), nameof(HashedName))]
+    [Index(nameof(CommentID), nameof(HashedName), nameof(IsDisabled))]
+    [Index(nameof(CommentID), nameof(HashedName), nameof(IsDisabled), nameof(ID))]
+    public class CommentImage
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = default!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = default!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = default!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("이미지 ID")]
+        public Guid UUID { get; set; } = Guid.NewGuid();
+
+        [Comment("원본 파일명")]
+        [StringLength(255)]
+        public string FileName { get; set; } = null!;
+
+        [Comment("저장 파일명")]
+        [StringLength(255)]
+        public string HashedName { get; set; } = null!;
+
+        [Comment("저장 경로")]
+        [StringLength(500)]
+        public string Path { get; set; } = null!;
+
+        [Comment("URL")]
+        [StringLength(1000)]
+        public string Url { get; set; } = null!;
+
+        [Comment("확장자")]
+        [StringLength(10)]
+        public string? Extension { get; set; }
+
+        [Comment("MIME 타입")]
+        [StringLength(100)]
+        public string? ContentType { get; set; }
+
+        [Comment("용량(byte)")]
+        public long? Size { get; set; }
+
+        [Comment("가로 해상도(px)")]
+        public short? Width { get; set; }
+
+        [Comment("세로 해상도(px)")]
+        public short? Height { get; set; }
+
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [Comment("비활성 일시")]
+        public DateTime? DisabledAt { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 61 - 0
Domain/Entities/Forum/Comments/CommentLink.cs

@@ -0,0 +1,61 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentLink")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(Url))]
+    [Index(nameof(CommentID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(IsDisabled), nameof(ID))]
+    public class CommentLink
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = default!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = default!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = default!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("링크 ID")]
+        public Guid UUID { get; set; }
+
+        [Comment("URL")]
+        [StringLength(1000)]
+        public string Url { get; set; } = default!;
+
+        [Comment("클릭 수")]
+        [Range(0, int.MaxValue)]
+        public int Clicks { get; set; } = 0;
+
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [Comment("비활성 일시")]
+        public DateTime? DisabledAt { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 54 - 0
Domain/Entities/Forum/Comments/CommentMedia.cs

@@ -0,0 +1,54 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentMedia")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(Url))]
+    [Index(nameof(CommentID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(IsDisabled), nameof(ID))]
+    public class CommentMedia
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("URL")]
+        [StringLength(1000)]
+        public string Url { get; set; } = null!;
+
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [Comment("비활성 일시")]
+        public DateTime? DisabledAt { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 62 - 0
Domain/Entities/Forum/Comments/CommentMention.cs

@@ -0,0 +1,62 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentMention")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(CommentID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(MemberID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(MemberID), nameof(Start), IsUnique = true)]
+    public class CommentMention
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public virtual Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("언급된 회원 ID")]
+        public int MemberID { get; set; }
+
+        [Comment("원문 회원 언급값")]
+        [MaxLength(64)]
+        public string RawHandle { get; set; } = null!;
+
+        [Comment("본문 내 시작 인덱스")]
+        public int Start { get; set; } = -1;
+
+        [Comment("길이")]
+        public int Length { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 69 - 0
Domain/Entities/Forum/Comments/CommentReaction.cs

@@ -0,0 +1,69 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Domain.Entities.Forum.ValueObject;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentReaction")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(Reaction))]
+    [Index(nameof(CreatedAt))]
+    [Index(nameof(CommentID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(MemberID), IsUnique = true)]
+    public class CommentReaction
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public virtual Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [Comment("반응 구분")]
+        [EnumDataType(typeof(Reaction), ErrorMessage = "{0}는 {1} 중 하나를 선택하세요.")]
+        public Reaction Reaction { get; set; }
+
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("수정 일시")]
+        public DateTime? UpdatedAt { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 84 - 0
Domain/Entities/Forum/Comments/CommentReport.cs

@@ -0,0 +1,84 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Domain.Entities.Forum.ValueObject;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Comments
+{
+    [Table("CommentReport")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(CommentID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(Type))]
+    [Index(nameof(Status))]
+    [Index(nameof(CreatedAt))]
+    [Index(nameof(CommentID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(MemberID), IsUnique = true)]
+    public class CommentReport
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = default!;
+
+        [ForeignKey(nameof(PostID))]
+        public virtual Post Post { get; set; } = default!;
+
+        [ForeignKey(nameof(CommentID))]
+        public virtual Comment Comment { get; set; } = default!;
+
+        [ForeignKey(nameof(MemberID))]
+        public virtual Member Member { get; set; } = default!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [Comment("신고 사유")]
+        [EnumDataType(typeof(ReportType), ErrorMessage = "{0}는 {1} 중 하나를 선택하세요.")]
+        public ReportType Type { get; set; }
+
+        [Comment("신고 내용")]
+        [StringLength(1000)]
+        public string? Reason { get; set; }
+
+        [Comment("처리 상태")]
+        [EnumDataType(typeof(ReportStatus), ErrorMessage = "{0}는 {1} 중 하나를 선택하세요.")]
+        public ReportStatus Status { get; set; } = ReportStatus.Received;
+
+        [Comment("처리 내용")]
+        [StringLength(1000)]
+        public string? Memo { get; set; }
+
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("수정 일시")]
+        public DateTime? UpdatedAt { get; set; }
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 22 - 0
Domain/Entities/Forum/Constants/BoardConstant.cs

@@ -0,0 +1,22 @@
+namespace Domain.Entities.Forum.Constants;
+
+/// <summary>
+/// 게시판 설정 상수
+/// </summary>
+public static class BoardConstant
+{
+    /// <summary>
+    /// 말머리 제한 수
+    /// </summary>
+    public const byte MaxPrefixCount = 10;
+
+    /// <summary>
+    /// 기본 출력 게시글 수
+    /// </summary>
+    public const int DefaultPerPage = 10;
+
+    /// <summary>
+    /// 최대 출력 게시글 수
+    /// </summary>
+    public const byte MaxPerPage = 100;
+}

+ 52 - 0
Domain/Entities/Forum/Constants/CommentConstant.cs

@@ -0,0 +1,52 @@
+namespace Domain.Entities.Forum.Constants;
+
+/// <summary>
+/// 댓글 설정 상수
+/// </summary>
+public static class CommentConstants
+{
+    /// <summary>
+    /// 기본 출력 댓글 수
+    /// </summary>
+    public const int DefaultPerPage = 10;
+
+    /// <summary>
+    /// 최대 출력 댓글 수
+    /// </summary>
+    public const byte MaxPerPage = 50;
+
+    /// <summary>
+    /// 최대 댓글 길이
+    /// </summary>
+    public const ushort MaxAllowedContentLength = 10000;
+
+    /// <summary>
+    /// 최대 이미지 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedImages = 10;
+
+    /// <summary>
+    /// 최대 이미지 용량 (KB)
+    /// </summary>
+    public const int MaxAllowedImageSize = 51200;
+
+    /// <summary>
+    /// 허용 이미지 확장자
+    /// </summary>
+    public const string AllowedImageExtensions = "jpg,jpeg,gif,png";
+
+    /// <summary>
+    /// 최대 미디어 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedMedias = 20;
+
+    /// <summary>
+    /// 최대 파일 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedFiles = 5;
+
+    /// <summary>
+    /// 최대 파일 용량 (KB)
+    /// </summary>
+    public const int MaxAllowedFileSize = 204800;
+}

+ 52 - 0
Domain/Entities/Forum/Constants/PostConstant.cs

@@ -0,0 +1,52 @@
+namespace Domain.Entities.Forum.Constants;
+
+/// <summary>
+/// 게시글 설정 상수
+/// </summary>
+public static class PostConstants
+{
+    /// <summary>
+    /// 최대 제목 길이
+    /// </summary>
+    public const byte MaxAllowedSubjectLength = 120;
+
+    /// <summary>
+    /// 최대 본문 길이
+    /// </summary>
+    public const ushort MaxAllowedContentLength = 5000;
+
+    /// <summary>
+    /// 최대 태그 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedTags = 10;
+
+    /// <summary>
+    /// 최대 이미지 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedImages = 50;
+
+    /// <summary>
+    /// 최대 이미지 용량 (KB)
+    /// </summary>
+    public const int MaxAllowedImageSize = 51200;
+
+    /// <summary>
+    /// 허용 이미지 확장자
+    /// </summary>
+    public const string AllowedImageExtensions = "jpg,jpeg,gif,png";
+
+    /// <summary>
+    /// 최대 미디어 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedMedias = 20;
+
+    /// <summary>
+    /// 최대 파일 입력 제한 수
+    /// </summary>
+    public const byte MaxAllowedFiles = 10;
+
+    /// <summary>
+    /// 최대 파일 용량 (KB)
+    /// </summary>
+    public const int MaxAllowedFileSize = 204800;
+}

+ 51 - 0
Domain/Entities/Forum/Logs/CommentFileDownLog.cs

@@ -0,0 +1,51 @@
+using Domain.Entities.Forum.Comments;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Logs
+{
+    [Table("CommentFileDownLog")]
+    [Index(nameof(CommentID))]
+    [Index(nameof(CommentFileID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(CommentID), nameof(CommentFileID))]
+    [Index(nameof(CommentID), nameof(CommentFileID), nameof(MemberID))]
+    public class CommentFileDownLog
+    {
+        [ForeignKey(nameof(CommentID))]
+        public Comment Comment { get; set; } = null!;
+
+        [ForeignKey(nameof(CommentFileID))]
+        public CommentFile CommentFile { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("댓글 파일 ID")]
+        public int CommentFileID { get; set; }
+
+        [Comment("회원 ID")]
+        public int? MemberID { get; set; }
+
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 51 - 0
Domain/Entities/Forum/Logs/CommentLinkClickLog.cs

@@ -0,0 +1,51 @@
+using Domain.Entities.Forum.Comments;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Logs
+{
+    [Table("CommentLinkClickLog")]
+    [Index(nameof(CommentID))]
+    [Index(nameof(CommentLinkID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(CommentID), nameof(CommentLinkID))]
+    [Index(nameof(CommentID), nameof(CommentLinkID), nameof(MemberID))]
+    public class CommentLinkClickLog
+    {
+        [ForeignKey(nameof(CommentID))]
+        public Comment Comment { get; set; } = null!;
+
+        [ForeignKey(nameof(CommentLinkID))]
+        public CommentLink CommentLink { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("댓글 링크 ID")]
+        public int CommentLinkID { get; set; }
+
+        [Comment("회원 ID")]
+        public int? MemberID { get; set; }
+
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 54 - 0
Domain/Entities/Forum/Logs/CommentUpdateLog.cs

@@ -0,0 +1,54 @@
+using Domain.Entities.Forum.Comments;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Logs
+{
+    [Table("CommentUpdateLog")]
+    [Index(nameof(CommentID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(ContentDiff))]
+    [Index(nameof(CommentID), nameof(ID))]
+    [Index(nameof(CommentID), nameof(MemberID))]
+    [Index(nameof(CommentID), nameof(MemberID), nameof(ID))]
+    public class CommentUpdateLog
+    {
+        [ForeignKey(nameof(CommentID))]
+        public Comment Comment { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("댓글 ID")]
+        public int CommentID { get; set; }
+
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [Comment("변경 내용")]
+        [StringLength(4000)]
+        public string? ContentDiff { get; set; }
+
+        [Comment("비고")]
+        [StringLength(200)]
+        public string? Note { get; set; }
+
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 60 - 0
Domain/Entities/Forum/Logs/PostFileDownLog.cs

@@ -0,0 +1,60 @@
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Logs
+{
+    [Table("PostFileDownLog")]
+    [Index(nameof(PostID))]
+    [Index(nameof(PostFileID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(PostID), nameof(PostFileID))]
+    [Index(nameof(PostID), nameof(PostFileID), nameof(MemberID))]
+    public class PostFileDownLog
+    {
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(PostFileID))]
+        public PostFile PostFile { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("게시글 파일 ID")]
+        [Comment("게시글 파일 ID")]
+        public int PostFileID { get; set; }
+
+        [DisplayName("회원 ID")]
+        [Comment("회원 ID")]
+        public int? MemberID { get; set; }
+
+        [DisplayName("IP Address")]
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [DisplayName("User-Agent")]
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 60 - 0
Domain/Entities/Forum/Logs/PostLinkClickLog.cs

@@ -0,0 +1,60 @@
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Logs
+{
+    [Table("PostLinkClickLog")]
+    [Index(nameof(PostID))]
+    [Index(nameof(PostLinkID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(PostID), nameof(PostLinkID))]
+    [Index(nameof(PostID), nameof(PostLinkID), nameof(MemberID))]
+    public class PostLinkClickLog
+    {
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(PostLinkID))]
+        public PostLink PostLink { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("게시글 파일 ID")]
+        [Comment("게시글 파일 ID")]
+        public int PostLinkID { get; set; }
+
+        [DisplayName("회원 ID")]
+        [Comment("회원 ID")]
+        public int? MemberID { get; set; }
+
+        [DisplayName("IP Address")]
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [DisplayName("User-Agent")]
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 72 - 0
Domain/Entities/Forum/Logs/PostUpdateLog.cs

@@ -0,0 +1,72 @@
+using Domain.Entities.Forum.Posts;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Logs
+{
+    [Table("PostUpdateLog")]
+    [Index(nameof(PostID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(SubjectDiff))]
+    [Index(nameof(ContentDiff))]
+    [Index(nameof(PostID), nameof(ID))]
+    [Index(nameof(PostID), nameof(MemberID))]
+    [Index(nameof(PostID), nameof(MemberID), nameof(ID))]
+    public class PostUpdateLog
+    {
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("회원 ID")]
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [DisplayName("변경 제목")]
+        [Comment("변경 제목")]
+        [DataType(DataType.Text)]
+        [StringLength(4000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? SubjectDiff { get; set; }
+
+        [DisplayName("변경 내용")]
+        [Comment("변경 내용")]
+        [DataType(DataType.Html)]
+        [StringLength(4000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? ContentDiff { get; set; }
+
+        [DisplayName("비고")]
+        [Comment("비고")]
+        [StringLength(200, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? Note { get; set; }
+
+        [DisplayName("IP Address")]
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; }
+
+        [DisplayName("User-Agent")]
+        [Comment("User-Agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; }
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 181 - 0
Domain/Entities/Forum/Posts/Post.cs

@@ -0,0 +1,181 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.Comments;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table(nameof(Post))]
+    [Index(nameof(BoardID))]
+    [Index(nameof(BoardPrefixID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(ID), nameof(BoardID))]
+    [Index(nameof(ID), nameof(BoardID), nameof(IsDeleted))]
+    [Index(nameof(ID), nameof(BoardID), nameof(BoardPrefixID), nameof(IsDeleted), nameof(CreatedAt))]
+    [Index(nameof(ID), nameof(BoardID), nameof(BoardPrefixID), nameof(IsDeleted), nameof(Views))]
+    [Index(nameof(ID), nameof(BoardID), nameof(BoardPrefixID), nameof(IsDeleted), nameof(Comments))]
+    [Index(nameof(ID), nameof(BoardID), nameof(BoardPrefixID), nameof(IsDeleted), nameof(Likes))]
+    public class Post
+    {
+        [ForeignKey(nameof(BoardID))]
+        public virtual Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(BoardPrefixID))]
+        public virtual BoardPrefix? BoardPrefix { get; set; }
+
+        [ForeignKey(nameof(MemberID))]
+        public virtual Member Member { get; set; } = null!;
+
+        // 게시글 댓글
+        public virtual List<Comment> Comment { get; set; } = [];
+
+        // 게시글 이미지
+        public virtual List<PostImage> PostImage { get; set; } = [];
+
+        // 게시글 미디어
+        public virtual List<PostMedia> PostMedia { get; set; } = [];
+
+        // 게시글 파일
+        public virtual List<PostFile> PostFile { get; set; } = [];
+
+        // 게시글 링크
+        public virtual List<PostLink> PostLink { get; set; } = [];
+
+        // 게시글 태그
+        public virtual List<PostTag> PostTag { get; set; } = [];
+
+        // 게시글 반응
+        public virtual List<PostReaction> PostReaction { get; set; } = [];
+
+        // 게시글 즐겨찾기
+        public virtual List<PostBookmark> PostBookmark { get; set; } = [];
+
+        // 게시글 신고
+        public virtual List<PostReport> PostReport { get; set; } = [];
+
+        // 게시글 파일 다운로드 기록
+        public virtual List<PostFileDownLog> PostFileDownLog { get; set; } = [];
+
+        // 게시글 링크 클릭 기록
+        public virtual List<PostLinkClickLog> PostLinkClickLog { get; set; } = [];
+
+        // 게시글 변경 기록
+        public virtual List<PostUpdateLog> PostUpdateLog { get; set; } = [];
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [Comment("게시글 말머리 ID")]
+        public int? BoardPrefixID { get; set; } = null;
+
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [Comment("제목")]
+        [StringLength(255)]
+        public string Subject { get; set; } = default!;
+
+        [Comment("내용")]
+        [StringLength(8000)]
+        public string Content { get; set; } = default!;
+
+        [Comment("회원 SID")]
+        [StringLength(20)]
+        public string? SID { get; set; }
+
+        [Comment("회원 이메일")]
+        [StringLength(60)]
+        public string? Email { get; set; }
+
+        [Comment("회원 이름")]
+        [StringLength(20)]
+        public string? Name { get; set; }
+
+        [Comment("대표 이미지")]
+        [StringLength(255)]
+        public string? Thumbnail { get; set; }
+
+        [Comment("답변 여부")]
+        public bool IsReply { get; set; } = false;
+
+        [Comment("익명 글 여부")]
+        public bool IsAnonymous { get; set; } = false;
+
+        [Comment("비밀글 여부")]
+        public bool IsSecret { get; set; } = false;
+
+        [Comment("일반 공지 여부")]
+        public bool IsNotice { get; set; } = false;
+
+        [Comment("전체 공지 여부")]
+        public bool IsSpeaker { get; set; } = false;
+
+        [Comment("삭제 여부")]
+        public bool IsDeleted { get; set; } = false;
+
+        [Comment("조회 수")]
+        public int Views { get; set; } = 0;
+
+        [Comment("좋아요")]
+        public int Likes { get; set; } = 0;
+
+        [Comment("싫어요")]
+        public int Dislikes { get; set; } = 0;
+
+        [Comment("댓글 수")]
+        public int Comments { get; set; } = 0;
+
+        [Comment("즐겨찾기 수")]
+        public int Bookmarks { get; set; } = 0;
+
+        [Comment("신고 수")]
+        public int Reports { get; set; } = 0;
+
+        [Comment("이미지 수")]
+        public byte Images { get; set; } = 0;
+
+        [Comment("미디어 수")]
+        public byte Medias { get; set; } = 0;
+
+        [Comment("파일 수")]
+        public byte Files { get; set; } = 0;
+
+        [Comment("Tag 수")]
+        public byte Tags { get; set; } = 0;
+
+        [Comment("IP")]
+        [StringLength(50)]
+        public string? IpAddress { get; set; }
+
+        [Comment("User-Agent")]
+        [StringLength(255)]
+        public string? UserAgent { get; set; }
+
+        [Comment("마지막 답변 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? LastReplyUpdatedAt { get; set; }
+
+        [Comment("마지막 댓글 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? LastCommentUpdatedAt { get; set; }
+
+        [Comment("삭제 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? DeletedAt { get; set; }
+
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; }
+
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 62 - 0
Domain/Entities/Forum/Posts/PostBookmark.cs

@@ -0,0 +1,62 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostBookmark")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(PostID), nameof(MemberID), IsUnique = true)]
+    [Index(nameof(BoardID), nameof(PostID), nameof(CreatedAt))]
+    [Index(nameof(BoardID), nameof(MemberID), nameof(CreatedAt))]
+    [Index(nameof(BoardID), nameof(PostID), nameof(MemberID), nameof(CreatedAt))]
+    public class PostBookmark
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("회원 ID")]
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [DisplayName("IP Address")]
+        [Comment("IP Address")]
+        [MaxLength(15)]
+        public string? IpAddress { get; set; } = null;
+
+        [DisplayName("User-agent")]
+        [Comment("User-agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 96 - 0
Domain/Entities/Forum/Posts/PostFile.cs

@@ -0,0 +1,96 @@
+using Domain.Entities.Forum.Boards;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostFile")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(UUID), IsUnique = true)]
+    [Index(nameof(PostID), nameof(HashedName))]
+    [Index(nameof(PostID), nameof(HashedName), nameof(IsDisabled))]
+    public class PostFile
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("파일 ID")]
+        [Comment("파일 ID")]
+        public Guid UUID { get; set; }
+
+        [DisplayName("원본 파일명")]
+        [Comment("원본 파일명")]
+        [StringLength(255, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string FileName { get; set; } = null!;
+
+        [DisplayName("저장 파일명")]
+        [Comment("저장 파일명")]
+        [StringLength(255, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string HashedName { get; set; } = null!;
+
+        [DisplayName("저장 경로")]
+        [Comment("저장 경로")]
+        [StringLength(500, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Path { get; set; } = null!;
+
+        [DisplayName("URL")]
+        [Comment("URL")]
+        [DataType(DataType.Url)]
+        [Url(ErrorMessage = "올바른 URL 형식이 아닙니다.")]
+        [StringLength(1000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Url { get; set; } = null!;
+
+        [DisplayName("확장자")]
+        [Comment("확장자")]
+        [StringLength(10, ErrorMessage = "{0}는 {1}자 이하로 입력하세요.")]
+        public string? Extension { get; set; } = null;
+
+        [DisplayName("MINE 타입")]
+        [Comment("MIME 타입")]
+        [StringLength(100, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? ContentType { get; set; } = null;
+
+        [DisplayName("용량(byte)")]
+        [Comment("용량(byte)")]
+        public long? Size { get; set; } = null;
+
+        [DisplayName("다운로드 수")]
+        [Comment("다운로드 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0}은 {2}자 이상, {1}자 이하로 입력하세요.")]
+        public int Downloads { get; set; } = 0;
+
+        [DisplayName("비활성 여부")]
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [DisplayName("비활성 일시")]
+        [Comment("비활성 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? DisabledAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 99 - 0
Domain/Entities/Forum/Posts/PostImage.cs

@@ -0,0 +1,99 @@
+using Domain.Entities.Forum.Boards;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostImage")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(UUID), IsUnique = true)]
+    [Index(nameof(PostID), nameof(HashedName))]
+    [Index(nameof(PostID), nameof(HashedName), nameof(IsDisabled))]
+    public class PostImage
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("이미지 ID")]
+        [Comment("이미지 ID")]
+        public Guid UUID { get; set; } = Guid.NewGuid();
+
+        [DisplayName("원본 파일명")]
+        [Comment("원본 파일명")]
+        [StringLength(255, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string FileName { get; set; } = null!;
+
+        [DisplayName("저장 파일명")]
+        [Comment("저장 파일명")]
+        [StringLength(255, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string HashedName { get; set; } = null!;
+
+        [DisplayName("저장 경로")]
+        [Comment("저장 경로")]
+        [StringLength(500, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Path { get; set; } = null!;
+
+        [DisplayName("URL")]
+        [Comment("URL")]
+        [DataType(DataType.ImageUrl)]
+        [Url(ErrorMessage = "올바른 URL 형식이 아닙니다.")]
+        [StringLength(1000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Url { get; set; } = null!;
+
+        [DisplayName("확장자")]
+        [Comment("확장자")]
+        [StringLength(10, ErrorMessage = "{0}는 {1}자 이하로 입력하세요.")]
+        public string? Extension { get; set; } = null;
+
+        [DisplayName("MINE 타입")]
+        [Comment("MIME 타입")]
+        [StringLength(100, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? ContentType { get; set; } = null;
+
+        [DisplayName("용량(byte)")]
+        [Comment("용량(byte)")]
+        public long? Size { get; set; } = null;
+
+        [DisplayName("가로 해상도(px)")]
+        [Comment("가로 해상도(px)")]
+        public short? Width { get; set; } = null;
+
+        [DisplayName("세로 해상도(px)")]
+        [Comment("세로 해상도(px)")]
+        public short? Height { get; set; } = null;
+
+        [DisplayName("비활성 여부")]
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [DisplayName("비활성 일시")]
+        [Comment("비활성 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? DisabledAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 66 - 0
Domain/Entities/Forum/Posts/PostLink.cs

@@ -0,0 +1,66 @@
+using Domain.Entities.Forum.Boards;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostLink")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(Url))]
+    [Index(nameof(PostID), nameof(CreatedAt))]
+    public class PostLink
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("링크 ID")]
+        [Comment("링크 ID")]
+        public Guid UUID { get; set; }
+
+        [DisplayName("URL")]
+        [Comment("URL")]
+        [DataType(DataType.Url)]
+        [Url(ErrorMessage = "올바른 URL 형식이 아닙니다.")]
+        [StringLength(1000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Url { get; set; } = null!;
+
+        [DisplayName("클릭 수")]
+        [Comment("클릭 수")]
+        [Range(0, int.MaxValue, ErrorMessage = "{0}은 {2}자 이상, {1}자 이하로 입력해 주세요.")]
+        public int Clicks { get; set; } = 0;
+
+        [DisplayName("비활성 여부")]
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [DisplayName("비활성 일시")]
+        [Comment("비활성 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? DisabledAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 57 - 0
Domain/Entities/Forum/Posts/PostMedia.cs

@@ -0,0 +1,57 @@
+using Domain.Entities.Forum.Boards;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostMedia")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(Url))]
+    [Index(nameof(PostID), nameof(CreatedAt))]
+    public class PostMedia
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("URL")]
+        [Comment("URL")]
+        [DataType(DataType.Url)]
+        [Url(ErrorMessage = "올바른 URL 형식이 아닙니다.")]
+        [StringLength(1000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string Url { get; set; } = null!;
+
+        [DisplayName("비활성 여부")]
+        [Comment("비활성 여부")]
+        public bool IsDisabled { get; set; } = false;
+
+        [DisplayName("비활성 일시")]
+        [Comment("비활성 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? DisabledAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 71 - 0
Domain/Entities/Forum/Posts/PostReaction.cs

@@ -0,0 +1,71 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Members;
+using Domain.Entities.Forum.ValueObject;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostReaction")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(Reaction))]
+    [Index(nameof(PostID), nameof(MemberID), IsUnique = true)]
+    public class PostReaction
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("회원 ID")]
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [DisplayName("반응 구분")]
+        [Comment("반응 구분")]
+        [EnumDataType(typeof(Reaction), ErrorMessage = "{0}는 {1} 중 하나를 선택하세요.")]
+        public Reaction Reaction { get; set; }
+
+        [DisplayName("IP Address")]
+        [Comment("IP Address")]
+        [MaxLength(50)]
+        public string? IpAddress { get; set; } = null;
+
+        [DisplayName("User-agent")]
+        [Comment("User-agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; } = null;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 87 - 0
Domain/Entities/Forum/Posts/PostReport.cs

@@ -0,0 +1,87 @@
+using Domain.Entities.Forum.Boards;
+using Domain.Entities.Forum.ValueObject;
+using Domain.Entities.Members;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostReport")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(MemberID))]
+    [Index(nameof(Type))]
+    [Index(nameof(Status))]
+    [Index(nameof(PostID), nameof(MemberID), IsUnique = true)]
+    public class PostReport
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(MemberID))]
+        public Member Member { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("회원 ID")]
+        [Comment("회원 ID")]
+        public int MemberID { get; set; }
+
+        [DisplayName("신고 사유")]
+        [Comment("신고 사유")]
+        [EnumDataType(typeof(ReportType), ErrorMessage = "{0}는 {1} 중 하나를 선택하세요.")]
+        public ReportType Type { get; set; }
+
+        [DisplayName("신고 내용")]
+        [Comment("신고 내용")]
+        [StringLength(1000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? Reason { get; set; } = null;
+
+        [DisplayName("처리 상태")]
+        [Comment("처리 상태")]
+        [EnumDataType(typeof(ReportStatus), ErrorMessage = "{0}는 {1} 중 하나를 선택하세요.")]
+        public ReportStatus Status { get; set; } = ReportStatus.Received;
+
+        [DisplayName("처리 내용")]
+        [Comment("처리 내용")]
+        [StringLength(1000, ErrorMessage = "{0}은 {1}자 이하로 입력하세요.")]
+        public string? Memo { get; set; } = null;
+
+        [DisplayName("IP Address")]
+        [Comment("IP Address")]
+        [MaxLength(15)]
+        public string? IpAddress { get; set; } = null;
+
+        [DisplayName("User-agent")]
+        [Comment("User-agent")]
+        [MaxLength(255)]
+        public string? UserAgent { get; set; } = null;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 47 - 0
Domain/Entities/Forum/Posts/PostTag.cs

@@ -0,0 +1,47 @@
+using Domain.Entities.Forum.Boards;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("PostTag")]
+    [Index(nameof(BoardID))]
+    [Index(nameof(PostID))]
+    [Index(nameof(TagID))]
+    public class PostTag
+    {
+        [ForeignKey(nameof(BoardID))]
+        public Board Board { get; set; } = null!;
+
+        [ForeignKey(nameof(PostID))]
+        public Post Post { get; set; } = null!;
+
+        [ForeignKey(nameof(TagID))]
+        public Tag Tag { get; set; } = null!;
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [DisplayName("게시판 ID")]
+        [Comment("게시판 ID")]
+        public int BoardID { get; set; }
+
+        [DisplayName("게시글 ID")]
+        [Comment("게시글 ID")]
+        public int PostID { get; set; }
+
+        [DisplayName("태그 ID")]
+        [Comment("태그 ID")]
+        public int TagID { get; set; }
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 47 - 0
Domain/Entities/Forum/Posts/Tag.cs

@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+
+namespace Domain.Entities.Forum.Posts
+{
+    [Table("Tag")]
+    [Index(nameof(Name), Name = "IX_Tag_Name", IsUnique = true)]
+    [Index(nameof(Slug), Name = "IX_Tag_Slug", IsUnique = true)]
+    [Index(nameof(UsageCount), Name = "IX_Tag_UsageCount")]
+    [Index(nameof(Name), nameof(Slug), Name = "IX_Tag_Name_Slug")]
+    public class Tag
+    {
+        public List<PostTag> PostTag { get; set; } = new List<PostTag>();
+
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        [DisplayName("PK")]
+        [Comment("PK")]
+        public int ID { get; set; }
+
+        [Required(ErrorMessage = "태그 명은 필수입니다.")]
+        [DisplayName("태그 명")]
+        [Comment("태그 명")]
+        [StringLength(50, ErrorMessage = "태그는 50자 이내로 입력해주세요.")]
+        public string Name { get; set; } = null!;
+
+        [Required(ErrorMessage = "슬러그는 필수입니다.")]
+        [DisplayName("슬러그")]
+        [Comment("슬러그")]
+        [StringLength(50, ErrorMessage = "Slug는 50자 이내로 입력해주세요.")]
+        public string Slug { get; set; } = null!;
+
+        [DisplayName("사용 횟수")]
+        [Comment("사용 횟수")]
+        public uint UsageCount { get; set; } = 0;
+
+        [DisplayName("수정 일시")]
+        [Comment("수정 일시")]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록 일시")]
+        [Comment("등록 일시")]
+        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+    }
+}

+ 18 - 0
Domain/Entities/Forum/ValueObject/BoardLayout.cs

@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 레이아웃 종류
+/// </summary>
+public enum BoardLayout : byte
+{
+    [Display(Name = "일반")]
+    Default = 0,
+
+    [Display(Name = "사진/동영상")]
+    Media = 1,
+
+    [Display(Name = "1:1 문의")]
+    QnA = 2
+}

+ 18 - 0
Domain/Entities/Forum/ValueObject/BoardMetaType.cs

@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 설정 메타 타입
+/// </summary>
+public enum BoardMetaType : byte
+{
+    [Display(Name = "게시판")]
+    Board = 0,
+
+    [Display(Name = "게시글")]
+    Post = 1,
+
+    [Display(Name = "댓글")]
+    Comment = 2
+}

+ 25 - 0
Domain/Entities/Forum/ValueObject/BoardNotify.cs

@@ -0,0 +1,25 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 알림 대상 (다중 선택 지원)
+/// </summary>
+[Flags]
+public enum BoardNotify : byte
+{
+    [Display(Name = "최고관리자")]
+    Admin = 1 << 0,
+
+    [Display(Name = "게시판 관리자")]
+    Manager = 1 << 1,
+
+    [Display(Name = "게시글 작성자")]
+    PostAuthor = 1 << 2,
+
+    [Display(Name = "댓글 작성자")]
+    CommentAuthor = 1 << 3,
+
+    [Display(Name = "답글 작성자")]
+    ReplyAuthor = 1 << 4
+}

+ 18 - 0
Domain/Entities/Forum/ValueObject/BoardPermission.cs

@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 권한 레벨
+/// </summary>
+public enum BoardPermission : short
+{
+    [Display(Name = "-1: 비회원")]
+    Guest = -1,
+
+    [Display(Name = "0: 정회원")]
+    Member = 0,
+
+    [Display(Name = "1000: 최고관리자")]
+    Admin = 1000
+}

+ 21 - 0
Domain/Entities/Forum/ValueObject/BoardSearch.cs

@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 검색 조건
+/// </summary>
+public enum BoardSearch : byte
+{
+    [Display(Name = "제목")]
+    Subject = 0,
+
+    [Display(Name = "내용")]
+    Content = 1,
+
+    [Display(Name = "작성자")]
+    Author = 2,
+
+    [Display(Name = "댓글")]
+    Comment = 3
+}

+ 21 - 0
Domain/Entities/Forum/ValueObject/BoardSort.cs

@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 정렬 조건
+/// </summary>
+public enum BoardSort : byte
+{
+    [Display(Name = "최신순")]
+    CreatedAt = 0,
+
+    [Display(Name = "조회순")]
+    Views = 1,
+
+    [Display(Name = "댓글순")]
+    Comments = 2,
+
+    [Display(Name = "공감순")]
+    Likes = 3
+}

+ 27 - 0
Domain/Entities/Forum/ValueObject/BoardType.cs

@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시판 종류
+/// </summary>
+public enum BoardType : byte
+{
+    [Display(Name = "자유 게시판")]
+    Free = 1,
+
+    [Display(Name = "문의 게시판")]
+    QnA = 2,
+
+    [Display(Name = "공지사항")]
+    Notice = 3,
+
+    [Display(Name = "익명 게시판")]
+    Anonymity = 4,
+
+    [Display(Name = "상품 게시판")]
+    Goods = 5,
+
+    [Display(Name = "사진 게시판")]
+    Gallery = 6
+}

+ 15 - 0
Domain/Entities/Forum/ValueObject/CommentSort.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 댓글 정렬 조건
+/// </summary>
+public enum CommentSort : byte
+{
+    [Display(Name = "최신순")]
+    CreatedAt = 0,
+
+    [Display(Name = "인기순")]
+    Score = 1
+}

+ 30 - 0
Domain/Entities/Forum/ValueObject/DisplayStatus.cs

@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 게시글 또는 댓글 상태
+/// </summary>
+public enum DisplayStatus : byte
+{
+    [Display(Name = "정상")]
+    Normal = 0,
+
+    [Display(Name = "나만보기")]
+    Hidden = 1,
+
+    [Display(Name = "본인 삭제")]
+    DeletedByOwner = 2,
+
+    [Display(Name = "운영자 삭제")]
+    RemovedByAdmin = 3,
+
+    [Display(Name = "운영자 숨김")]
+    HiddenByAdmin = 4,
+
+    [Display(Name = "신고로 숨김")]
+    HiddenByReport = 5,
+
+    [Display(Name = "대기")]
+    Pending = 6
+}

+ 15 - 0
Domain/Entities/Forum/ValueObject/Reaction.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 반응 구분
+/// </summary>
+public enum Reaction : byte
+{
+    [Display(Name = "좋아요")]
+    Like = 1,
+
+    [Display(Name = "싫어요")]
+    Dislike = 2
+}

+ 18 - 0
Domain/Entities/Forum/ValueObject/ReportStatus.cs

@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 신고 상태
+/// </summary>
+public enum ReportStatus : byte
+{
+    [Display(Name = "접수")]
+    Received = 0,
+
+    [Display(Name = "처리 중")]
+    Processing = 1,
+
+    [Display(Name = "조치 완료")]
+    Completed = 2
+}

+ 38 - 0
Domain/Entities/Forum/ValueObject/ReportType.cs

@@ -0,0 +1,38 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.Forum.ValueObject;
+
+/// <summary>
+/// 신고 유형
+/// </summary>
+public enum ReportType : byte
+{
+    None = 0,
+
+    [Display(Name = "욕설")]
+    Abuse = 1,
+
+    [Display(Name = "음란")]
+    Obscene = 2,
+
+    [Display(Name = "불법")]
+    Illegal = 3,
+
+    [Display(Name = "신분 사칭")]
+    Impersonation = 4,
+
+    [Display(Name = "현금거래유도")]
+    CashTrade = 5,
+
+    [Display(Name = "스팸/광고")]
+    SpamAd = 6,
+
+    [Display(Name = "도배")]
+    Flood = 7,
+
+    [Display(Name = "개인정보노출")]
+    PersonalLeak = 8,
+
+    [Display(Name = "기타")]
+    Other = 9
+}

+ 0 - 0
Infrastructure/Infrastructure/Persistence/Migrations/20260205091913_a3.Designer.cs → Infrastructure/Persistence/Migrations/20260205091913_a3.Designer.cs


+ 0 - 0
Infrastructure/Infrastructure/Persistence/Migrations/20260205091913_a3.cs → Infrastructure/Persistence/Migrations/20260205091913_a3.cs


+ 0 - 0
Infrastructure/Infrastructure/Persistence/Migrations/20260206143102_a4.Designer.cs → Infrastructure/Persistence/Migrations/20260206143102_a4.Designer.cs


+ 0 - 0
Infrastructure/Infrastructure/Persistence/Migrations/20260206143102_a4.cs → Infrastructure/Persistence/Migrations/20260206143102_a4.cs