From c458cfe4e78a29678861f78c80c0e0424baf18a1 Mon Sep 17 00:00:00 2001 From: SEAN Date: Wed, 25 Feb 2026 17:36:14 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20Tag=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=8B=A0=EC=84=A4=20=EB=B0=8F=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=ED=99=95=EC=A0=95=20(#243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tag 엔티티 생성 (ServiceId, Name, Description, CreatedAt, CreatedBy) - ITagRepository 인터페이스 및 TagRepository 구현 - TagConfiguration: Unique Index (ServiceId, Name), FK Restrict - Service.TagList Navigation 추가 - ErrorCodes에 Tag 에러코드 4종 추가 (191~194) - AppDbContext DbSet, DI 등록 - EF Core Migration AddTagTable 생성 및 적용 Closes #243 --- SPMS.Domain/Common/ErrorCodes.cs | 6 + SPMS.Domain/Entities/Service.cs | 1 + SPMS.Domain/Entities/Tag.cs | 15 + SPMS.Domain/Interfaces/ITagRepository.cs | 9 + SPMS.Infrastructure/AppDbContext.cs | 1 + SPMS.Infrastructure/DependencyInjection.cs | 1 + .../20260225083135_AddTagTable.Designer.cs | 1190 +++++++++++++++++ .../Migrations/20260225083135_AddTagTable.cs | 67 + .../Migrations/AppDbContextModelSnapshot.cs | 60 + .../Configurations/TagConfiguration.cs | 34 + .../Persistence/Repositories/TagRepository.cs | 18 + 11 files changed, 1402 insertions(+) create mode 100644 SPMS.Domain/Entities/Tag.cs create mode 100644 SPMS.Domain/Interfaces/ITagRepository.cs create mode 100644 SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.Designer.cs create mode 100644 SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.cs create mode 100644 SPMS.Infrastructure/Persistence/Configurations/TagConfiguration.cs create mode 100644 SPMS.Infrastructure/Persistence/Repositories/TagRepository.cs diff --git a/SPMS.Domain/Common/ErrorCodes.cs b/SPMS.Domain/Common/ErrorCodes.cs index 96bc07d..0fb55d5 100644 --- a/SPMS.Domain/Common/ErrorCodes.cs +++ b/SPMS.Domain/Common/ErrorCodes.cs @@ -59,4 +59,10 @@ public static class ErrorCodes public const string FileNotFound = "181"; public const string FileTypeNotAllowed = "182"; public const string FileSizeExceeded = "183"; + + // === Tag (9) === + public const string TagNotFound = "191"; + public const string TagNameDuplicate = "192"; + public const string TagNameImmutable = "193"; + public const string TagLimitExceeded = "194"; } diff --git a/SPMS.Domain/Entities/Service.cs b/SPMS.Domain/Entities/Service.cs index f6936b4..6b1a500 100644 --- a/SPMS.Domain/Entities/Service.cs +++ b/SPMS.Domain/Entities/Service.cs @@ -35,4 +35,5 @@ public class Service : BaseEntity public ICollection ServiceIps { get; set; } = new List(); public ICollection Devices { get; set; } = new List(); public ICollection Messages { get; set; } = new List(); + public ICollection TagList { get; set; } = new List(); } diff --git a/SPMS.Domain/Entities/Tag.cs b/SPMS.Domain/Entities/Tag.cs new file mode 100644 index 0000000..79bcc52 --- /dev/null +++ b/SPMS.Domain/Entities/Tag.cs @@ -0,0 +1,15 @@ +namespace SPMS.Domain.Entities; + +public class Tag : BaseEntity +{ + public long ServiceId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public DateTime CreatedAt { get; set; } + public long CreatedBy { get; set; } + public DateTime? UpdatedAt { get; set; } + + // Navigation + public Service Service { get; set; } = null!; + public Admin CreatedByAdmin { get; set; } = null!; +} diff --git a/SPMS.Domain/Interfaces/ITagRepository.cs b/SPMS.Domain/Interfaces/ITagRepository.cs new file mode 100644 index 0000000..b757089 --- /dev/null +++ b/SPMS.Domain/Interfaces/ITagRepository.cs @@ -0,0 +1,9 @@ +using SPMS.Domain.Entities; + +namespace SPMS.Domain.Interfaces; + +public interface ITagRepository : IRepository +{ + Task GetByIdWithServiceAsync(long id); + Task ExistsInServiceAsync(long serviceId, string name); +} diff --git a/SPMS.Infrastructure/AppDbContext.cs b/SPMS.Infrastructure/AppDbContext.cs index 2d554ad..5a27fbe 100644 --- a/SPMS.Infrastructure/AppDbContext.cs +++ b/SPMS.Infrastructure/AppDbContext.cs @@ -25,6 +25,7 @@ public class AppDbContext : DbContext public DbSet Banners => Set(); public DbSet Faqs => Set(); public DbSet AppConfigs => Set(); + public DbSet Tags => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/SPMS.Infrastructure/DependencyInjection.cs b/SPMS.Infrastructure/DependencyInjection.cs index 81e109c..4b0ea89 100644 --- a/SPMS.Infrastructure/DependencyInjection.cs +++ b/SPMS.Infrastructure/DependencyInjection.cs @@ -43,6 +43,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // External Services services.AddScoped(); diff --git a/SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.Designer.cs b/SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.Designer.cs new file mode 100644 index 0000000..6a60cb0 --- /dev/null +++ b/SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.Designer.cs @@ -0,0 +1,1190 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SPMS.Infrastructure; + +#nullable disable + +namespace SPMS.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260225083135_AddTagTable")] + partial class AddTagTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SPMS.Domain.Entities.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdminCode") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("varchar(8)"); + + b.Property("AgreePrivacy") + .HasColumnType("tinyint(1)"); + + b.Property("AgreeTerms") + .HasColumnType("tinyint(1)"); + + b.Property("AgreedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EmailVerified") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("EmailVerifiedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LastLoginAt") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("RefreshToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Role") + .HasColumnType("tinyint"); + + b.Property("TempPasswordIssuedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdminCode") + .IsUnique(); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Admin", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.AppConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ConfigKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ConfigValue") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LinkType") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("LinkUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Position") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.ToTable("Banner", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FailCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("OpenCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("SentCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("StatDate") + .HasColumnType("date"); + + b.Property("SuccessCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "StatDate") + .IsUnique(); + + b.ToTable("DailyStat", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AgreeUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("AppVersion") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceModel") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("DeviceToken") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("MarketingAgreed") + .HasColumnType("tinyint(1)"); + + b.Property("MktAgreeUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OsVersion") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Platform") + .HasColumnType("tinyint"); + + b.Property("PushAgreed") + .HasColumnType("tinyint(1)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Tags") + .HasColumnType("json"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "DeviceToken"); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("Question") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.ToTable("Faq", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceId"); + + b.ToTable("File", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("CustomData") + .HasColumnType("json"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LinkType") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("LinkUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("MessageCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MessageCode") + .IsUnique(); + + b.HasIndex("ServiceId"); + + b.ToTable("Message", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Notice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("IsPinned") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("PaidAt") + .HasColumnType("datetime(6)"); + + b.Property("PaymentKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PaymentMethod") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.Property("TierAfter") + .HasColumnType("tinyint"); + + b.Property("TierBefore") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Payment", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("MessageId") + .HasColumnType("bigint"); + + b.Property("OpenedAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("MessageId"); + + b.HasIndex("ServiceId", "OpenedAt"); + + b.ToTable("PushOpenLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("FailReason") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("MessageId") + .HasColumnType("bigint"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("MessageId"); + + b.HasIndex("ServiceId", "SentAt"); + + b.ToTable("PushSendLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ApiKeyCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApnsAuthType") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ApnsBundleId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ApnsCertExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("ApnsCertPassword") + .HasColumnType("text"); + + b.Property("ApnsCertificate") + .HasColumnType("text"); + + b.Property("ApnsKeyId") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ApnsPrivateKey") + .HasColumnType("text"); + + b.Property("ApnsTeamId") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("FcmCredentials") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ServiceCode") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("varchar(8)"); + + b.Property("ServiceName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.Property("SubStartedAt") + .HasColumnType("datetime(6)"); + + b.Property("SubTier") + .HasColumnType("tinyint"); + + b.Property("Tags") + .HasColumnType("json"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WebhookEvents") + .HasColumnType("longtext"); + + b.Property("WebhookUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceCode") + .IsUnique(); + + b.ToTable("Service", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.ToTable("ServiceIp", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("AdminId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasColumnType("json"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("TargetType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ServiceId"); + + b.ToTable("SystemLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceId", "Name") + .IsUnique(); + + b.ToTable("Tag", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EventType") + .HasColumnType("tinyint"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("json"); + + b.Property("ResponseBody") + .HasColumnType("text"); + + b.Property("ResponseCode") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.Property("WebhookUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "SentAt"); + + 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") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Device", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("Devices") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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") + .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.Message", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("Messages") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + 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") + .WithMany() + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Admin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b => + { + b.HasOne("SPMS.Domain.Entities.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Message", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Message"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b => + { + b.HasOne("SPMS.Domain.Entities.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Message", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Message"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Service", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("ServiceIps") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "Admin") + .WithMany() + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Admin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Tag", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("TagList") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Service", b => + { + b.Navigation("Devices"); + + b.Navigation("Messages"); + + b.Navigation("ServiceIps"); + + b.Navigation("TagList"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.cs b/SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.cs new file mode 100644 index 0000000..874fa14 --- /dev/null +++ b/SPMS.Infrastructure/Migrations/20260225083135_AddTagTable.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SPMS.Infrastructure.Migrations +{ + /// + public partial class AddTagTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tag", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ServiceId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "varchar(200)", maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "bigint", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tag", x => x.Id); + table.ForeignKey( + name: "FK_Tag_Admin_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Admin", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Tag_Service_ServiceId", + column: x => x.ServiceId, + principalTable: "Service", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Tag_CreatedBy", + table: "Tag", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Tag_ServiceId_Name", + table: "Tag", + columns: new[] { "ServiceId", "Name" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tag"); + } + } +} diff --git a/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 250eb93..f6a570e 100644 --- a/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -836,6 +836,45 @@ namespace SPMS.Infrastructure.Migrations b.ToTable("SystemLog", (string)null); }); + modelBuilder.Entity("SPMS.Domain.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceId", "Name") + .IsUnique(); + + b.ToTable("Tag", (string)null); + }); + modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b => { b.Property("Id") @@ -1102,6 +1141,25 @@ namespace SPMS.Infrastructure.Migrations b.Navigation("Service"); }); + modelBuilder.Entity("SPMS.Domain.Entities.Tag", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("TagList") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + b.Navigation("Service"); + }); + modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b => { b.HasOne("SPMS.Domain.Entities.Service", "Service") @@ -1120,6 +1178,8 @@ namespace SPMS.Infrastructure.Migrations b.Navigation("Messages"); b.Navigation("ServiceIps"); + + b.Navigation("TagList"); }); #pragma warning restore 612, 618 } diff --git a/SPMS.Infrastructure/Persistence/Configurations/TagConfiguration.cs b/SPMS.Infrastructure/Persistence/Configurations/TagConfiguration.cs new file mode 100644 index 0000000..0fdeaae --- /dev/null +++ b/SPMS.Infrastructure/Persistence/Configurations/TagConfiguration.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SPMS.Domain.Entities; + +namespace SPMS.Infrastructure.Persistence.Configurations; + +public class TagConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Tag"); + + builder.HasKey(e => e.Id); + builder.Property(e => e.Id).ValueGeneratedOnAdd(); + + builder.Property(e => e.Name).HasMaxLength(50).IsRequired(); + builder.Property(e => e.Description).HasMaxLength(200); + builder.Property(e => e.CreatedAt).IsRequired(); + builder.Property(e => e.CreatedBy).IsRequired(); + builder.Property(e => e.UpdatedAt); + + builder.HasOne(e => e.Service) + .WithMany(s => s.TagList) + .HasForeignKey(e => e.ServiceId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(e => e.CreatedByAdmin) + .WithMany() + .HasForeignKey(e => e.CreatedBy) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasIndex(e => new { e.ServiceId, e.Name }).IsUnique(); + } +} diff --git a/SPMS.Infrastructure/Persistence/Repositories/TagRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/TagRepository.cs new file mode 100644 index 0000000..402953b --- /dev/null +++ b/SPMS.Infrastructure/Persistence/Repositories/TagRepository.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using SPMS.Domain.Entities; +using SPMS.Domain.Interfaces; + +namespace SPMS.Infrastructure.Persistence.Repositories; + +public class TagRepository : Repository, ITagRepository +{ + public TagRepository(AppDbContext context) : base(context) { } + + public async Task GetByIdWithServiceAsync(long id) + => await _dbSet + .Include(t => t.Service) + .FirstOrDefaultAsync(t => t.Id == id); + + public async Task ExistsInServiceAsync(long serviceId, string name) + => await _dbSet.AnyAsync(t => t.ServiceId == serviceId && t.Name == name); +}