정책 기반 "백그라운드 스크리닝 버튼 노출/숨김" 구현 가이드

  • 8 minutes to read

1) 개요

이 문서는 Azunt.Web이라는 ASP.NET Core MVC 프로젝트를 생성한 뒤, 테넌트/파트너/권한/정책에 따라 뷰에서 버튼을 표시하거나 숨기는 기능을 구현하는 과정을 단계별로 설명합니다. 설정(Options) → 정책(Policy) → 컨트롤러/뷰 순으로 연결하여 최종 버튼 노출 여부를 결정합니다.


2) 사전 준비

  • .NET 8 SDK 이상

  • Visual Studio 2022 또는 VS Code

  • Entity Framework Core CLI

    dotnet tool install --global dotnet-ef
    

3) 프로젝트 생성

mkdir C:\Azunt\src\mvc\backgroundcheck
cd /d C:\Azunt\src\mvc\backgroundcheck

# MVC + 개별 사용자 인증(Identity) 포함
dotnet new mvc -n Azunt.Web --auth Individual

cd Azunt.Web

필요 시(대부분 템플릿에 포함되지만 누락 시) 패키지 설치:

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

4) 디렉터리/파일 구조

아래 파일을 생성(또는 교체)합니다.

Azunt.Web\Azunt.Web\
 ├─ appsettings.json
 ├─ Program.cs
 ├─ Constants\BackgroundProviders.cs
 ├─ Models\ScreeningDemoVm.cs
 ├─ Policies\IBackgroundScreeningPolicy.cs
 ├─ Policies\BackgroundScreeningPolicy.cs
 ├─ Settings\BackgroundScreeningOptions.cs
 ├─ Controllers\ScreeningDemoController.cs
 └─ Views\ScreeningDemo\Index.cshtml

Views\ScreeningDemo 폴더를 새로 만든 뒤 Index.cshtml을 추가합니다.


5) 설정 파일 (appsettings.json)

5-1) BackgroundScreening 섹션 포함

Azunt.Web\Azunt.Web\appsettings.json

{
    // ========================== ▼ 배경조회(Background Checks) API 설정 시작 ▼ ==========================
    "BackgroundScreening": {
        //[0] 배경조회 API 기본 URL
        // - 샌드박스/운영 환경에 따라 달라질 수 있음
        // - 예: "https://sandbox.dotnetnote.com" 또는 "https://api.vendor.com"
        "ApiBaseUrl": "https://www.dotnetnote.com",

        //[1] API Prefix 경로
        // - 기본값은 "/v1" 사용
        // - 벤더에서 버전이 올라가면 "/v2" 등으로 교체 가능
        "ApiPrefix": "/v1",

        //[2] 인증 토큰(JWT)
        // - 샌드박스 환경: 고정 "TestToken" 제공
        // - 운영 환경: 실제 발급받은 토큰을 주입
        // - 보안을 위해 환경 변수나 KeyVault에서 로드하는 방식 권장
        "JwtToken": "TestToken",

        // 서비스별 사용/허용 테넌트 정책
        "Providers": {
            "Azunt": {
                "Enabled": true,
                "AllowedTenants": [ "VisualAcademy", "DotNetNote" ]
            },
            "DevLec": {
                "Enabled": true,
                "AllowedTenants": [] // 빈 배열 = 모두 허용
            }
        }
    },
    // ========================== ▲ 배경조회(Background Checks) API 설정 종료 ▲ ==========================

    "ConnectionStrings": {
        "DefaultConnection": "DataSource=app.db;Cache=Shared"
    },

5-2) 실행용(주석 제거 + 보일러플레이트 추가)

실제 실행 시 사용할 appsettings.json 예시

{
  "BackgroundScreening": {
    "ApiBaseUrl": "https://www.dotnetnote.com",
    "ApiPrefix": "/v1",
    "JwtToken": "TestToken",
    "Providers": {
      "Azunt": {
        "Enabled": true,
        "AllowedTenants": [ "VisualAcademy", "DotNetNote" ]
      },
      "DevLec": {
        "Enabled": true,
        "AllowedTenants": []
      }
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "DataSource=app.db;Cache=Shared"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

6) 정책/옵션/상수/모델/컨트롤러/뷰 구현

6-1) 정책 인터페이스

Azunt.Web\Azunt.Web\Policies\IBackgroundScreeningPolicy.cs

namespace Azunt.Web.Policies;

public interface IBackgroundScreeningPolicy
{
    /// <summary>
    /// 지정한 Provider가 현재 tenantName에서 노출/사용 가능한지 여부
    /// </summary>
    bool IsProviderVisible(string provider, string? tenantName);
}

6-2) 옵션 클래스

Azunt.Web\Azunt.Web\Settings\BackgroundScreeningOptions.cs

using System.Collections.Generic;

namespace Azunt.Web.Settings;

public sealed class BackgroundScreeningOptions
{
    public string ApiBaseUrl { get; set; } = "";
    public string ApiPrefix { get; set; } = "/v1";
    public string JwtToken { get; set; } = "";

    // key: ProviderName (예: "Azunt", "DevLec")
    public Dictionary<string, BackgroundProviderOptions> Providers { get; set; } = new();
}

public sealed class BackgroundProviderOptions
{
    public bool Enabled { get; set; } = false;

    /// <summary>
    /// 허용 테넌트 화이트리스트.
    /// 빈 배열이면 "모두 허용"으로 정책에서 해석합니다.
    /// </summary>
    public List<string> AllowedTenants { get; set; } = new();
}

6-3) 정책 구현

Azunt.Web\Azunt.Web\Policies\BackgroundScreeningPolicy.cs

using Azunt.Web.Settings;
using Microsoft.Extensions.Options;
using System;
using System.Linq;

namespace Azunt.Web.Policies;

public sealed class BackgroundScreeningPolicy : IBackgroundScreeningPolicy
{
    private readonly IOptionsSnapshot<BackgroundScreeningOptions> _opts;

    public BackgroundScreeningPolicy(IOptionsSnapshot<BackgroundScreeningOptions> opts)
        => _opts = opts;

    public bool IsProviderVisible(string provider, string? tenantName)
    {
        if (string.IsNullOrWhiteSpace(provider)) return false;

        var providers = _opts.Value.Providers;

        // 대/소문자 무시로 안전하게 조회
        var p = providers
            .FirstOrDefault(kv => string.Equals(kv.Key, provider, StringComparison.OrdinalIgnoreCase))
            .Value;

        if (p is null || !p.Enabled) return false;

        // AllowedTenants 비었으면 모두 허용
        if (p.AllowedTenants is null || p.AllowedTenants.Count == 0)
            return true;

        if (string.IsNullOrWhiteSpace(tenantName))
            return false;

        return p.AllowedTenants.Any(t =>
            string.Equals(t, tenantName, StringComparison.OrdinalIgnoreCase));
    }
}

6-4) Provider 상수

Azunt.Web\Azunt.Web\Constants\BackgroundProviders.cs

namespace Azunt.Web.Constants;

public static class BackgroundProviders
{
    public const string Azunt = "Azunt";
    public const string DevLec = "DevLec";
}

6-5) Program.cs (DI 구성)

Azunt.Web\Azunt.Web\Program.cs

using Azunt.Web.Data;
using Azunt.Web.Policies;
using Azunt.Web.Settings;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();

#region Background Service
// appsettings 바인딩
builder.Services.Configure<BackgroundScreeningOptions>(
    builder.Configuration.GetSection("BackgroundScreening"));

// 정책 서비스 DI
builder.Services.AddScoped<IBackgroundScreeningPolicy, BackgroundScreeningPolicy>();
#endregion

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

app.MapRazorPages()
   .WithStaticAssets();

app.Run();

6-6) ViewModel

Azunt.Web\Azunt.Web\Models\ScreeningDemoVm.cs

namespace Azunt.Web.Models
{
    public sealed class ScreeningDemoVm
    {
        public string? TenantName { get; set; }      // 예: VisualAcademy, DotNetNote, Hawaso
        public string? PartnerName { get; set; }     // 현재 테넌트의 기본 파트너명 (있다면)
        public bool IsAdmin { get; set; }            // 관리자 여부(테스트용)
        public bool IsGlobalAdmin { get; set; }      // 글로벌 관리자 정책 여부(테스트용)
    }
}

6-7) 컨트롤러

Azunt.Web\Azunt.Web\Controllers\ScreeningDemoController.cs

using Microsoft.AspNetCore.Mvc;

namespace DotNetNote.Controllers;

public sealed class ScreeningDemoController : Controller
{
    public IActionResult Index(string? tenant, string? partner, bool? admin, bool? global)
    {
        var vm = new ScreeningDemoVm
        {
            TenantName = string.IsNullOrWhiteSpace(tenant) ? "VisualAcademy" : tenant,
            PartnerName = string.IsNullOrWhiteSpace(partner) ? "Azunt" : partner,
            IsAdmin = admin ?? false,
            IsGlobalAdmin = global ?? false
        };

        return View(vm);
    }
}

6-8) 뷰

Azunt.Web\Azunt.Web\Views\ScreeningDemo\Index.cshtml

@model ScreeningDemoVm
@using Azunt.Web.Models
@using Azunt.Web.Policies
@using Azunt.Web.Constants
@inject IBackgroundScreeningPolicy ScreeningPolicy

@{
    ViewData["Title"] = "Background Screening Demo";

    // ▼ 공통 변수명만 사용 (값은 Constants로부터)
    var providerKey = BackgroundProviders.DevLec; // 테스트 대상 프로바이더
    var tenantName = Model.TenantName;
    var partnerName = Model.PartnerName;

    // 정책 기반 노출 여부
    bool showByPolicy =
        ScreeningPolicy.IsProviderVisible(providerKey, tenantName);

    // 파트너명 매칭 (대/소문자 무시)
    bool isCurrentPartner =
        !string.IsNullOrWhiteSpace(partnerName)
        && providerKey.Equals(partnerName, System.StringComparison.OrdinalIgnoreCase);

    // 최종 버튼 노출 조건: 관리자 OR 글로벌관리자 OR 현재파트너 OR 정책일치
    bool showButton =
        Model.IsAdmin || Model.IsGlobalAdmin || isCurrentPartner || showByPolicy;
}

<h2>@ViewData["Title"]</h2>

<div style="display:flex; gap: 8px; flex-wrap: wrap;">
    <div style="display:flex; gap: 8px; align-items:center;">
        <button id="addNewBackgroundCheck"
                class="btn btn-primary"
                onclick="alert('Azunt clicked')">
            Add Background Check <sup>@BackgroundProviders.Azunt</sup>
        </button>

        @if (showButton)
        {
            <button id="addNewBackgroundCheckProvider"
                    class="btn btn-success"
                    onclick="alert('@providerKey clicked')">
                Add Background Check <sup>@providerKey</sup>
            </button>
        }
    </div>
</div>

<hr />

<h4>Diagnostics</h4>
<ul>
    <li><b>TenantName</b>: @tenantName</li>
    <li><b>PartnerName</b>: @partnerName</li>
    <li><b>IsAdmin</b>: @Model.IsAdmin</li>
    <li><b>IsGlobalAdmin</b>: @Model.IsGlobalAdmin</li>
    <li><b>ProviderKey(Test)</b>: @providerKey</li>
    <li><b>VisibleByPolicy</b>: @showByPolicy</li>
    <li><b>IsCurrentPartner</b>: @isCurrentPartner</li>
    <li><b>Final ShowButton</b>: @showButton</li>
</ul>

7) 데이터베이스 초기화(Identity)

dotnet ef migrations add InitialCreate
dotnet ef database update

8) 실행 및 검증

dotnet run

테스트 URL 예시

  • 기본(기본값: Tenant=VisualAcademy, Partner=Azunt, Admin/Global=false) https://localhost:{PORT}/ScreeningDemo

  • 정책 허용(DevLec의 AllowedTenants = 빈 배열 → 모든 테넌트 허용) https://localhost:{PORT}/ScreeningDemo?tenant=Hawaso

  • 관리자 권한 강제 노출 https://localhost:{PORT}/ScreeningDemo?admin=true

  • 글로벌 관리자 권한 강제 노출 https://localhost:{PORT}/ScreeningDemo?global=true

  • 파트너 일치 확인(뷰 상단 providerKeyBackgroundProviders.Azunt로 바꿔 테스트) https://localhost:{PORT}/ScreeningDemo?partner=Azunt

Diagnostics 섹션에서 판정 근거(VisibleByPolicy, IsCurrentPartner, Final ShowButton)를 확인합니다.


9) 동작 원리

  • 설정(appsettings.json) 각 Provider에 대해 Enabled, AllowedTenants를 정의합니다. AllowedTenants가 빈 배열이면 모든 테넌트 허용으로 해석합니다.
  • 정책(Policy) IsProviderVisible(provider, tenant)가 위 규칙에 따라 노출 가능 여부를 반환합니다.
  • 뷰 로직 최종 노출 조건: IsAdmin || IsGlobalAdmin || IsCurrentPartner || VisibleByPolicy

10) 확장 포인트

  • 여러 Provider를 동시에 렌더링하려면 BackgroundScreeningOptions.Providers를 순회하며 버튼을 생성합니다.
  • 정책에 사용자 역할, 시간대, 기능 플래그 등의 추가 조건을 결합할 수 있습니다.
  • 운영 환경에서는 JwtToken을 환경 변수나 Key Vault에서 로드하도록 보안 강화를 권장합니다.

11) 문제 해결

  • JSON 파싱 오류: 주석 제거 버전을 사용합니다.
  • ApplicationDbContext 누락: --auth Individual 템플릿으로 생성했는지 확인하거나 Identity 스캐폴딩을 수행합니다.
  • 마이그레이션 오류: DefaultConnection을 확인 후 dotnet ef migrations add InitialCreatedotnet ef database update 순으로 초기화합니다.
더 깊이 공부하고 싶다면
DevLec에서는 실무 중심의 C#, .NET, ASP.NET Core, Blazor, 데이터 액세스 강좌를 단계별로 제공합니다. 현재 수강 가능한 강좌 외에도 더 많은 과정이 준비되어 있습니다.
DevLec.com에서 자세한 커리큘럼을 확인해 보세요.
DevLec 공식 강의
C# Programming
C# 프로그래밍 입문
프로그래밍을 처음 시작하는 입문자를 위한 C# 기본기 완성 과정입니다.
ASP.NET Core 10.0
ASP.NET Core 10.0 시작하기 MVC Fundamentals Part 1 MVC Fundamentals Part 2
웹 애플리케이션의 구조와 MVC 패턴을 ASP.NET Core로 실습하며 익힐 수 있습니다.
Blazor Server
풀스택 웹개발자 과정 Part 1 풀스택 웹개발자 과정 Part 2 풀스택 웹개발자 과정 Part 3
실무에서 바로 활용 가능한 Blazor Server 기반 관리자·포털 프로젝트를 만들어 봅니다.
Data & APIs
Entity Framework Core 시작하기 ADO.NET Fundamentals Blazor Server Fundamentals Minimal APIs
데이터 액세스와 Web API를 함께 이해하면 실무 .NET 백엔드 개발에 큰 도움이 됩니다.
VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com