ASP.NET Core MVC 멀티 테넌트 Badge QR Code 페이지 구현하기

  • 6 minutes to read

개요

이 문서는 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 반환
더 깊이 공부하고 싶다면
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