정책 기반 "백그라운드 스크리닝 버튼 노출/숨김" 구현 가이드
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파트너 일치 확인(뷰 상단
providerKey를BackgroundProviders.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 InitialCreate→dotnet ef database update순으로 초기화합니다.