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

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