ASP.NET Core MVC 멀티 테넌트 Badge QR Code 페이지 구현하기
개요
이 문서는 ASP.NET Core MVC에서 멀티 테넌트 QR 코드 페이지를 구현하는 방법을 단계별로 설명합니다.
주요 특징
- 모든 테넌트를 하나의 컨트롤러로 처리
- 설정 기반 테넌트 관리
- SEO 친화적인 URL 구조: /badge/{tenant}
qrcode.js
QR 코드를 생성하기 위해서는 wwwroot/lib/qrcode.js 파일이 필요합니다.
해당 파일은 클라이언트에서 동적으로 QR 코드를 생성하는 데 사용되며, 각 URL을 시각적으로 표현할 수 있도록 도와줍니다.
이 파일이 없다면 AI 검색을 통해 확보하거나, 기존 강의 소스를 참고하여 추가할 수 있습니다.
1. 설정 (appsettings.json)
먼저 애플리케이션에서 사용할 기본 URL과 테넌트 목록을 설정 파일에 정의합니다.
이 방식은 코드 수정 없이 설정만으로 테넌트를 추가하거나 제거할 수 있다는 장점이 있습니다.
{
"BaseUrl": "https://portal.example.com",
"TenantSettings": {
"Tenants": [
"VisualAcademy",
"DevLec",
"DotNetNote"
]
}
}
설명
- BaseUrl은 IConfiguration으로 직접 읽습니다.
- TenantSettings는 Options 패턴으로 바인딩합니다.
2. TenantSettings 클래스
설정 파일의 TenantSettings 섹션을 바인딩하기 위한 클래스를 정의합니다.
이 클래스는 테넌트 목록을 코드에서 타입 안전하게 사용할 수 있도록 도와줍니다.
파일: Models/Configuration/TenantSettings.cs
namespace VisualAcademy.Models.Configuration
{
// 테넌트 목록 바인딩용 클래스
public class TenantSettings
{
public string[] Tenants { get; set; } = System.Array.Empty<string>();
}
}
3. ViewModel
뷰에서 사용할 데이터를 전달하기 위한 ViewModel을 정의합니다. 각 테넌트별로 생성되는 QR 코드 URL들을 하나의 개체로 묶어 관리할 수 있습니다.
파일: Models/Codes/BadgePageViewModel.cs
namespace VisualAcademy.Models.Codes
{
// Badge 페이지에서 사용할 ViewModel
public class BadgePageViewModel
{
public string Tenant { get; set; } = string.Empty;
// QR 코드 1: 메인 포털
public string CandidatePortalUrl { get; set; } = string.Empty;
// QR 코드 2: 정보 변경 페이지
public string ChangeOfInformationUrl { get; set; } = string.Empty;
// QR 코드 3: 테넌트별 등록 페이지
public string RegisterUrl { get; set; } = string.Empty;
public string PageTitle { get; set; } = string.Empty;
public string Heading { get; set; } = string.Empty;
}
}
4. Program.cs 설정
Options 패턴을 사용하여 TenantSettings를 DI 컨테이너에 등록합니다.
이렇게 하면 컨트롤러에서 설정 값을 쉽게 주입받아 사용할 수 있습니다.
using VisualAcademy.Models.Configuration;
var builder = WebApplication.CreateBuilder(args);
// 테넌트 설정 바인딩
builder.Services.Configure<TenantSettings>(
builder.Configuration.GetSection("TenantSettings"));
builder.Services.AddControllersWithViews();
var app = builder.Build();
5. BadgeController 구현
이 컨트롤러는 모든 테넌트 요청을 처리하는 핵심 구성 요소입니다.
URL의 {tenant} 값을 기반으로 유효성을 검사하고, 해당 테넌트에 맞는 QR 코드 데이터를 생성합니다.
파일: Codes/BadgeController.cs
using System;
using System.Linq;
using VisualAcademy.Models.Codes;
using VisualAcademy.Models.Configuration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace VisualAcademy.Codes
{
[AllowAnonymous]
[Route("badge")]
public class BadgeController : Controller
{
private readonly TenantSettings _tenantSettings;
private readonly IConfiguration _configuration;
public BadgeController(
IOptions<TenantSettings> tenantSettings,
IConfiguration configuration)
{
_tenantSettings = tenantSettings.Value ?? new TenantSettings();
_configuration = configuration;
}
// GET /badge
[HttpGet("")]
public IActionResult Index()
{
// 테넌트 목록은 노출하지 않고 안내 페이지만 표시
return View();
}
// GET /badge/{tenant}
[HttpGet("{tenant}")]
public IActionResult Tenant(string tenant)
{
if (string.IsNullOrWhiteSpace(tenant))
{
return NotFound();
}
var normalizedTenant = tenant.Trim();
// 테넌트 검증 (대소문자 무시)
var matchedTenant = (_tenantSettings.Tenants ?? Array.Empty<string>())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(x => x.Equals(normalizedTenant, StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrWhiteSpace(matchedTenant))
{
return NotFound();
}
// BaseUrl 직접 읽기
var baseUrl = _configuration["BaseUrl"]?.Trim();
if (string.IsNullOrWhiteSpace(baseUrl))
{
baseUrl = "https://portal.example.com";
}
var model = new BadgePageViewModel
{
Tenant = matchedTenant,
CandidatePortalUrl = baseUrl,
ChangeOfInformationUrl = $"{baseUrl}/Identity/Account/Manage/ChangeOfInformation",
RegisterUrl = $"{baseUrl}/Identity/Account/Register?Tenant={Uri.EscapeDataString(matchedTenant)}",
PageTitle = $"{matchedTenant} Badge QR Code",
Heading = $"{matchedTenant} Badge QR Code"
};
return View("Tenant", model);
}
}
}
NOTE
Uri.EscapeDataString은 문자열을 URL 쿼리 파라미터로 안전하게 전달할 수 있도록 특수문자를 인코딩하는 메서드로, 예를 들어 "My Tenant"는 "My%20Tenant"로 변환됩니다.
6. Index View
/badge 경로로 접근했을 때 표시되는 간단한 안내 페이지입니다.
이 페이지에서는 테넌트 목록을 노출하지 않도록 하여 보안 및 정보 노출을 방지합니다.
파일: Views/Badge/Index.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Badge QR Code</title>
</head>
<body>
<h1>Badge QR Code</h1>
<p>Please use a valid tenant-specific URL.</p>
</body>
</html>
7. Tenant View
실제 QR 코드가 표시되는 페이지입니다. 각 URL은 클릭 가능한 링크로 제공되며, 동시에 QR 코드로도 생성됩니다.
파일: Views/Badge/Tenant.cshtml
@model VisualAcademy.Models.Codes.BadgePageViewModel
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@Model.PageTitle</title>
<script src="~/lib/qrcode.js"></script>
</head>
<body>
<h1>@Model.Heading</h1>
<h3>Candidate Portal</h3>
<p>
<a href="@Model.CandidatePortalUrl" target="_blank" rel="noopener noreferrer">
@Model.CandidatePortalUrl
</a>
</p>
<div id="qrcode1"></div>
<hr />
<h3>Change of Information</h3>
<p>
<a href="@Model.ChangeOfInformationUrl" target="_blank" rel="noopener noreferrer">
@Model.ChangeOfInformationUrl
</a>
</p>
<div id="qrcode2"></div>
<hr />
<h3>Register Page</h3>
<p>
<a href="@Model.RegisterUrl" target="_blank" rel="noopener noreferrer">
@Model.RegisterUrl
</a>
</p>
<div id="qrcode3"></div>
<script type="text/javascript">
var url1 = "@Html.Raw(Model.CandidatePortalUrl)";
new QRCode(document.getElementById("qrcode1"), {
text: url1,
width: 128,
height: 128
});
var url2 = "@Html.Raw(Model.ChangeOfInformationUrl)";
new QRCode(document.getElementById("qrcode2"), {
text: url2,
width: 128,
height: 128
});
var url3 = "@Html.Raw(Model.RegisterUrl)";
new QRCode(document.getElementById("qrcode3"), {
text: url3,
width: 128,
height: 128
});
</script>
</body>
</html>
8. 테스트
구현이 완료되면 다음 URL을 통해 정상 동작 여부를 확인합니다.
- /badge → 안내 페이지 표시
- /badge/visualacademy → 정상 QR 페이지
- /badge/devlec → 정상 QR 페이지
- /badge/invalid → 404 반환