feat: Public API Entity 정의 및 DB 스키마 구축 (#76)
All checks were successful
SPMS_API/pipeline/head This commit looks good

Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/77
This commit is contained in:
김선규 2026-02-10 04:25:42 +00:00
commit c8c9a44b0f
12 changed files with 1721 additions and 0 deletions

View File

@ -0,0 +1,13 @@
namespace SPMS.Domain.Entities;
public class AppConfig : BaseEntity
{
public long ServiceId { get; set; }
public string ConfigKey { get; set; } = string.Empty;
public string? ConfigValue { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -0,0 +1,19 @@
namespace SPMS.Domain.Entities;
public class Banner : BaseEntity
{
public long ServiceId { get; set; }
public string Title { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public string? LinkUrl { get; set; }
public string? LinkType { get; set; }
public string Position { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -0,0 +1,16 @@
namespace SPMS.Domain.Entities;
public class Faq : BaseEntity
{
public long ServiceId { get; set; }
public string? Category { get; set; }
public string Question { get; set; } = string.Empty;
public string Answer { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -0,0 +1,18 @@
namespace SPMS.Domain.Entities;
public class Notice : BaseEntity
{
public long ServiceId { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public bool IsPinned { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public long CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
// Navigation
public Service Service { get; set; } = null!;
public Admin CreatedByAdmin { get; set; } = null!;
}

View File

@ -21,6 +21,10 @@ public class AppDbContext : DbContext
public DbSet<WebhookLog> WebhookLogs => Set<WebhookLog>();
public DbSet<SystemLog> SystemLogs => Set<SystemLog>();
public DbSet<Payment> Payments => Set<Payment>();
public DbSet<Notice> Notices => Set<Notice>();
public DbSet<Banner> Banners => Set<Banner>();
public DbSet<Faq> Faqs => Set<Faq>();
public DbSet<AppConfig> AppConfigs => Set<AppConfig>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SPMS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddPublicApiTables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppConfig",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
ConfigKey = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ConfigValue = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AppConfig", x => x.Id);
table.ForeignKey(
name: "FK_AppConfig_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Banner",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
Title = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ImageUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
LinkUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
LinkType = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Position = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Banner", x => x.Id);
table.ForeignKey(
name: "FK_Banner_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Faq",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
Category = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Question = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Answer = table.Column<string>(type: "text", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Faq", x => x.Id);
table.ForeignKey(
name: "FK_Faq_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Notice",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
Title = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Content = table.Column<string>(type: "text", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsPinned = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notice", x => x.Id);
table.ForeignKey(
name: "FK_Notice_Admin_CreatedBy",
column: x => x.CreatedBy,
principalTable: "Admin",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Notice_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_AppConfig_ServiceId_ConfigKey",
table: "AppConfig",
columns: new[] { "ServiceId", "ConfigKey" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Banner_ServiceId",
table: "Banner",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_Faq_ServiceId",
table: "Faq",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_Notice_CreatedBy",
table: "Notice",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_Notice_ServiceId",
table: "Notice",
column: "ServiceId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppConfig");
migrationBuilder.DropTable(
name: "Banner");
migrationBuilder.DropTable(
name: "Faq");
migrationBuilder.DropTable(
name: "Notice");
}
}
}

View File

@ -98,6 +98,99 @@ namespace SPMS.Infrastructure.Migrations
b.ToTable("Admin", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.AppConfig", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ConfigKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ConfigValue")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ServiceId", "ConfigKey")
.IsUnique();
b.ToTable("AppConfig", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Banner", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<string>("LinkType")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("LinkUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Position")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("Banner", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b =>
{
b.Property<long>("Id")
@ -207,6 +300,51 @@ namespace SPMS.Infrastructure.Migrations
b.ToTable("Device", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Faq", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Category")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<string>("Question")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("Faq", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b =>
{
b.Property<long>("Id")
@ -322,6 +460,59 @@ namespace SPMS.Infrastructure.Migrations
b.ToTable("Message", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Notice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<bool>("IsPinned")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("ServiceId");
b.ToTable("Notice", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
{
b.Property<long>("Id")
@ -648,6 +839,28 @@ namespace SPMS.Infrastructure.Migrations
b.ToTable("WebhookLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.AppConfig", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Banner", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
@ -670,6 +883,17 @@ namespace SPMS.Infrastructure.Migrations
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Faq", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
@ -708,6 +932,25 @@ namespace SPMS.Infrastructure.Migrations
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Notice", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "Admin")

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class AppConfigConfiguration : IEntityTypeConfiguration<AppConfig>
{
public void Configure(EntityTypeBuilder<AppConfig> builder)
{
builder.ToTable("AppConfig");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.ConfigKey).HasMaxLength(100).IsRequired();
builder.Property(e => e.ConfigValue).HasColumnType("text");
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.UpdatedAt);
builder.HasIndex(e => new { e.ServiceId, e.ConfigKey }).IsUnique();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class BannerConfiguration : IEntityTypeConfiguration<Banner>
{
public void Configure(EntityTypeBuilder<Banner> builder)
{
builder.ToTable("Banner");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.Title).HasMaxLength(100).IsRequired();
builder.Property(e => e.ImageUrl).HasMaxLength(500).IsRequired();
builder.Property(e => e.LinkUrl).HasMaxLength(500);
builder.Property(e => e.LinkType).HasMaxLength(20);
builder.Property(e => e.Position).HasMaxLength(50).IsRequired();
builder.Property(e => e.SortOrder).IsRequired();
builder.Property(e => e.IsActive).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(true);
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.UpdatedAt);
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class FaqConfiguration : IEntityTypeConfiguration<Faq>
{
public void Configure(EntityTypeBuilder<Faq> builder)
{
builder.ToTable("Faq");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.Category).HasMaxLength(50);
builder.Property(e => e.Question).HasMaxLength(500).IsRequired();
builder.Property(e => e.Answer).HasColumnType("text").IsRequired();
builder.Property(e => e.SortOrder).IsRequired();
builder.Property(e => e.IsActive).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(true);
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.UpdatedAt);
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class NoticeConfiguration : IEntityTypeConfiguration<Notice>
{
public void Configure(EntityTypeBuilder<Notice> builder)
{
builder.ToTable("Notice");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.Title).HasMaxLength(200).IsRequired();
builder.Property(e => e.Content).HasColumnType("text").IsRequired();
builder.Property(e => e.IsPinned).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.Property(e => e.IsActive).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(true);
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.CreatedBy).IsRequired();
builder.Property(e => e.UpdatedAt);
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.CreatedByAdmin)
.WithMany()
.HasForeignKey(e => e.CreatedBy)
.OnDelete(DeleteBehavior.Restrict);
builder.HasQueryFilter(e => !e.IsDeleted);
}
}