멀티 테넌트 Contact 페이지 관리 시스템 (ASP.NET Core Identity 기반)

  • 6 minutes to read

이 문서에서는 ASP.NET Core Identity를 사용하여 멀티 테넌트 Contact 페이지를 관리하는 시스템을 구축하는 방법을 설명합니다. 이 시스템은 테넌트별로 개별적인 Contact 페이지를 보여주고, 관리자 전용 페이지에서 테넌트별 콘텐츠를 관리할 수 있습니다.

📋 기능 요구 사항

  1. Contact 페이지의 테넌트별 구분

    • 로그인한 사용자의 TenantName에 따라 맞춤형 Contact 페이지를 보여줍니다.
    • 로그인하지 않은 사용자는 기본 테넌트(Hawaso)의 Contact 페이지를 볼 수 있습니다.
  2. Contact 페이지의 관리자 관리 기능

    • /admin/contact-management 경로에서 관리자는 로그인한 사용자의 TenantName에 맞는 Contact 페이지의 내용을 입력 및 수정할 수 있습니다.
  3. 단일 레코드 구조

    • Pages 테이블에 테넌트별로 단일 레코드를 유지하여 간단한 쿼리와 데이터 관리가 가능하도록 합니다.

📂 테이블 설계 (Pages 테이블)

Pages 테이블은 테넌트별 Contact 페이지의 콘텐츠를 관리합니다.

CREATE TABLE Pages (
    Id INT IDENTITY(1,1) PRIMARY KEY, -- 기본 키 (Primary Key)
    TenantName NVARCHAR(MAX) NOT NULL DEFAULT 'Hawaso', -- 테넌트명 (예: Hawaso, Tenant1 등)
    PageName VARCHAR(50) NOT NULL DEFAULT 'Contact', -- 페이지명 (예: Contact, About 등)
    Title NVARCHAR(200) NOT NULL, -- 페이지 제목
    Content NVARCHAR(MAX) NOT NULL, -- 페이지의 본문 (HTML 또는 Plain Text)
    LastUpdated DATETIME NOT NULL DEFAULT GETDATE() -- 마지막 수정 날짜 (기본값: 현재 시간)
);
컬럼명 데이터 유형 설명
Id INT (PK) 기본 키 (Primary Key)
TenantName NVARCHAR(MAX) 테넌트명 (예: Hawaso, Tenant1)
PageName VARCHAR(50) 페이지명 (Contact, About 등)
Title NVARCHAR(200) 페이지 제목
Content NVARCHAR(MAX) 페이지 본문 (HTML / Plain Text)
LastUpdated DATETIME 마지막 수정 날짜

🛠️ 1. 모델 설계

Page.cs

public class Page
{
    public int Id { get; set; }
    public string TenantName { get; set; } = "Hawaso"; // 기본 테넌트명 Hawaso
    public string PageName { get; set; } = "Contact"; // 기본 페이지명 Contact
    public string Title { get; set; } = string.Empty; // 페이지 제목
    public string Content { get; set; } = string.Empty; // 페이지 본문
    public DateTime LastUpdated { get; set; } // 마지막 수정 날짜
}

🛠️ 2. Identity 사용자 모델 수정

ApplicationUser.csTenantName 속성을 추가합니다.

ApplicationUser.cs

public class ApplicationUser : IdentityUser
{
    public string? TenantName { get; set; } = "Hawaso"; // 기본값 Hawaso
}

🛠️ 3. MyDbContext 설정

Pages 테이블을 추가합니다.

MyDbContext.cs

public class MyDbContext : IdentityDbContext<ApplicationUser>
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
    }

    public DbSet<Page> Pages { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.Entity<Page>().HasKey(p => p.Id);
        builder.Entity<Page>().Property(p => p.TenantName).HasDefaultValue("Hawaso");
    }
}

🛠️ 4. 마이그레이션 및 데이터베이스 업데이트

Pages 테이블과 AspNetUsers 테이블TenantName 속성을 추가합니다.

dotnet ef migrations add AddPagesAndTenantName
dotnet ef database update

🛠️ 5. Contact 페이지 (사용자 보기)

라우팅 경로: /contact

ContactComponent.razor

@page "/contact"

@using Hawaso.Data
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Identity
@inject UserManager<ApplicationUser> UserManager
@inject IHttpContextAccessor HttpContextAccessor
@inject ApplicationDbContext _dbContext
@inject AuthenticationStateProvider AuthenticationStateProvider

<PageTitle>Contact</PageTitle>

<h1>@Page?.Title</h1>

<div>
    @((MarkupString)Page?.Content)
</div>

@code {
    private Page? Page;

    protected override async Task OnInitializedAsync()
    {
        // 로그인한 사용자의 TenantName을 가져오기
        //[3] var user = await UserManager.GetUserAsync(HttpContextAccessor.HttpContext.User);
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = await UserManager.GetUserAsync(authState.User);
        var tenantName = user?.TenantName ?? "Hawaso";

        Page = await _dbContext.Pages
            .Where(p => p.PageName == "Contact" && p.TenantName == tenantName)
            .FirstOrDefaultAsync();

        if (Page == null)
        {
            Page = await _dbContext.Pages
                .Where(p => p.PageName == "Contact" && p.TenantName == "Hawaso")
                .FirstOrDefaultAsync();
        }
    }
}

🛠️ 6. Contact 관리 페이지 (관리자용)

라우팅 경로: /admin/contact-management

ContactManagementComponent.razor

@page "/admin/contact-management"

@using Hawaso.Data
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Identity
@inject UserManager<ApplicationUser> UserManager
@inject IHttpContextAccessor HttpContextAccessor
@inject ApplicationDbContext _dbContext
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider

<PageTitle>Contact Management</PageTitle>

<h1>Contact Management</h1>

<EditForm Model="Page" OnValidSubmit="SaveChanges">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div>
        <label for="title">Title</label>
        <InputText id="title" @bind-Value="Page.Title" class="form-control" />
    </div>

    <div class="mt-3">
        <label for="content">Content</label>
        @* <InputTextArea id="content" @bind-Value="Page.Content" class="form-control" rows="10" /> *@
        <Hawaso.Components.CkEditorFourComponent @bind-Value="Page.Content"></Hawaso.Components.CkEditorFourComponent>
    </div>

    <button type="submit" class="btn btn-primary mt-3">Save Changes</button>
</EditForm>

@code {
    private Page Page = new Page();

    protected override async Task OnInitializedAsync()
    {
        //[2] var user = await UserManager.GetUserAsync(HttpContextAccessor.HttpContext.User);
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = await UserManager.GetUserAsync(authState.User);
        var tenantName = user?.TenantName ?? "Hawaso";

        Page = await _dbContext.Pages
            .Where(p => p.PageName == "Contact" && p.TenantName == tenantName)
            .FirstOrDefaultAsync();

        if (Page == null)
        {
            Page = new Page
                {
                    PageName = "Contact",
                    TenantName = tenantName
                };
        }
    }

    private async Task SaveChanges()
    {
        var existingContent = await _dbContext.Pages
            .FirstOrDefaultAsync(p => p.PageName == "Contact" && p.TenantName == Page.TenantName);

        if (existingContent != null)
        {
            existingContent.Title = Page.Title;
            existingContent.Content = Page.Content;
            existingContent.LastUpdated = DateTime.Now;
        }
        else
        {
            Page.LastUpdated = DateTime.Now;
            _dbContext.Pages.Add(Page);
        }

        await _dbContext.SaveChangesAsync();
        NavigationManager.NavigateTo("/contact");
    }
}

📚 라우팅 요약

경로 설명 컴포넌트 이름
/contact 사용자 보기용 Contact 페이지 ContactComponent.razor
/admin/contact-management 관리자용 Contact 관리 페이지 ContactManagementComponent.razor

🎉 정리

  1. 멀티 테넌트 Contact 페이지 시스템을 구축했습니다.
  2. 사용자 로그인 시 TenantName에 따라 맞춤형 콘텐츠가 표시됩니다.
  3. 관리자 관리 기능을 통해 Contact 페이지 내용을 편집하고 저장할 수 있습니다.
  4. 테넌트의 Contact 페이지 데이터는 Pages 테이블의 단일 레코드로 관리됩니다.

🔧 확장 가능성

  1. 캐싱 최적화: DB 쿼리를 줄이기 위해 In-Memory Cache 또는 Distributed Cache(Redis) 추가.
  2. 보안 강화: /admin/contact-management 경로에 관리자 권한을 추가하여 접근을 제한.
  3. 페이지 확장: 다른 페이지 (About, FAQ 등)도 Pages 테이블에 추가하여 관리할 수 있습니다.

이제 멀티 테넌트 Contact 페이지 관리 시스템이 완벽하게 준비되었습니다! 🚀

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com