feat: complete Admin + Portal Blazor WebAssembly SPA architecture
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m17s
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m17s
Deployment: - Admin UI: /admin (Blazor WebAssembly, 219+ WASM files) - Portal: /portal (Blazor WebAssembly, standalone SPA) - Homepage: / (Razor Pages + SSR) - API: /api (FastEndpoints + JWT/Cookie auth) Features: - Admin: Full management dashboard + MudDataGrid - Portal: Login + basic customer dashboard (expandable) - Auth: Cookie-based (Portal) + JWT (Admin) - SEO: Sitemap (public content only), Naver verification Technical: - Dual WASM hosting (/admin and /portal) - SPA fallback routing for client-side navigation - Shared Application layer (services, DTOs) - Separate Client projects for isolation Production Ready: - Zero 빌드 오류 - 모든 배포 파일 준비됨 - Green-Blue 무중단 배포 지원 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>백원숙 세무회계 - 관리자</title>
|
||||||
|
<base href="/taxbaik/" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/taxbaik/favicon.svg" />
|
||||||
|
<link rel="alternate icon" href="/taxbaik/favicon.ico" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
|
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||||
|
<script>
|
||||||
|
window.taxbaikAdminBuildVersion = 'unknown';
|
||||||
|
window.taxbaikAdminComponent = 'AdminApp';
|
||||||
|
document.documentElement.classList.toggle(
|
||||||
|
'admin-login-route',
|
||||||
|
window.location.pathname.toLowerCase().endsWith('/admin/login'));
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="css/admin.css" />
|
||||||
|
<component type="typeof(HeadOutlet)" render-mode="InteractiveWebAssembly" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="components-reconnect-modal" class="admin-reconnect-modal">
|
||||||
|
<div class="admin-reconnect-card">
|
||||||
|
<strong>연결 재설정 중...</strong>
|
||||||
|
<span>새로운 버전으로 업데이트되었습니다.</span>
|
||||||
|
<span style="font-size: 0.85rem; margin-top: 0.5rem; opacity: 0.8;">자동으로 페이지를 새로고침합니다. 잠시만 기다려주세요.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="blazor-loading" class="blazor-loading-overlay">
|
||||||
|
<div class="blazor-loading-spinner">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>로드 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// 로그인 화면은 prerender로 즉시 표시되므로 스피너가 필요 없다.
|
||||||
|
// 그 외 인증 화면은 WASM 부팅이 끝날 때까지(AdminShell.OnAfterRenderAsync에서 hideLoading 호출)
|
||||||
|
// 스피너를 "업데이트 스플래시"로 보여준다.
|
||||||
|
if (!document.documentElement.classList.contains('admin-login-route')) {
|
||||||
|
var loadingOverlay = document.getElementById('blazor-loading');
|
||||||
|
if (loadingOverlay) loadingOverlay.classList.add('show');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
||||||
|
<Routes />
|
||||||
|
<script src="/taxbaik/_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
|
<script src="/taxbaik/js/admin-session.js"></script>
|
||||||
|
<script src="/taxbaik/_framework/blazor.webassembly.js"></script>
|
||||||
|
<script>
|
||||||
|
// admin-session.js 로드 완료 대기 후 초기화
|
||||||
|
function initSession() {
|
||||||
|
if (window.taxbaikAdminSession) {
|
||||||
|
if (typeof window.taxbaikAdminSession.initErrorLogging === 'function') {
|
||||||
|
window.taxbaikAdminSession.initErrorLogging();
|
||||||
|
}
|
||||||
|
if (typeof window.taxbaikAdminSession.bindLoginForm === 'function') {
|
||||||
|
window.taxbaikAdminSession.bindLoginForm();
|
||||||
|
}
|
||||||
|
if (typeof window.taxbaikAdminSession.watchReconnect === 'function') {
|
||||||
|
window.taxbaikAdminSession.watchReconnect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// admin-session.js가 아직 로드되지 않았으면 재시도
|
||||||
|
setTimeout(initSession, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initSession();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool isDarkMode = false;
|
||||||
|
private MudTheme mudTheme = new()
|
||||||
|
{
|
||||||
|
Palette = new PaletteLight()
|
||||||
|
{
|
||||||
|
Primary = "#1976D2",
|
||||||
|
PrimaryContrastText = "#FFFFFF",
|
||||||
|
Secondary = "#2D9F7E",
|
||||||
|
SecondaryContrastText = "#FFFFFF",
|
||||||
|
Tertiary = "#FF8A50",
|
||||||
|
TertiaryContrastText = "#FFFFFF",
|
||||||
|
Surface = "#F5F7FA",
|
||||||
|
Background = "#FFFFFF",
|
||||||
|
BackgroundGrey = "#F8F9FB",
|
||||||
|
DrawerBackground = "#FFFFFF",
|
||||||
|
DrawerText = "#424242",
|
||||||
|
AppbarBackground = "#FFFFFF",
|
||||||
|
AppbarText = "#424242",
|
||||||
|
TextPrimary = "#1A1A1A",
|
||||||
|
TextSecondary = "#64748B",
|
||||||
|
TextDisabled = "#94A3B8",
|
||||||
|
ActionDefault = "#1976D2",
|
||||||
|
ActionDisabled = "#BDBDBD",
|
||||||
|
Divider = "#E2E8F0",
|
||||||
|
DividerLight = "#F1F5F9",
|
||||||
|
Error = "#DC2626",
|
||||||
|
ErrorContrastText = "#FFFFFF",
|
||||||
|
Warning = "#F59E0B",
|
||||||
|
WarningContrastText = "#FFFFFF",
|
||||||
|
Info = "#06B6D4",
|
||||||
|
InfoContrastText = "#FFFFFF",
|
||||||
|
Success = "#16A34A",
|
||||||
|
SuccessContrastText = "#FFFFFF",
|
||||||
|
},
|
||||||
|
LayoutProperties = new LayoutProperties()
|
||||||
|
{
|
||||||
|
DefaultBorderRadius = "6px"
|
||||||
|
},
|
||||||
|
Typography = new Typography()
|
||||||
|
{
|
||||||
|
Default = new Default()
|
||||||
|
{
|
||||||
|
FontSize = ".8125rem",
|
||||||
|
FontWeight = 400,
|
||||||
|
LineHeight = 1.5
|
||||||
|
},
|
||||||
|
H1 = new H1()
|
||||||
|
{
|
||||||
|
FontSize = "1.75rem",
|
||||||
|
FontWeight = 600,
|
||||||
|
LineHeight = 1.2
|
||||||
|
},
|
||||||
|
H2 = new H2()
|
||||||
|
{
|
||||||
|
FontSize = "1.5rem",
|
||||||
|
FontWeight = 600,
|
||||||
|
LineHeight = 1.3
|
||||||
|
},
|
||||||
|
H3 = new H3()
|
||||||
|
{
|
||||||
|
FontSize = "1.25rem",
|
||||||
|
FontWeight = 600,
|
||||||
|
LineHeight = 1.3
|
||||||
|
},
|
||||||
|
H4 = new H4()
|
||||||
|
{
|
||||||
|
FontSize = "1.1rem",
|
||||||
|
FontWeight = 600,
|
||||||
|
LineHeight = 1.4
|
||||||
|
},
|
||||||
|
H5 = new H5()
|
||||||
|
{
|
||||||
|
FontSize = "0.95rem",
|
||||||
|
FontWeight = 500,
|
||||||
|
LineHeight = 1.4
|
||||||
|
},
|
||||||
|
H6 = new H6()
|
||||||
|
{
|
||||||
|
FontSize = "0.85rem",
|
||||||
|
FontWeight = 500,
|
||||||
|
LineHeight = 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
@Body
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<AdminShell>
|
||||||
|
<AdminTelemetryContext />
|
||||||
|
@Body
|
||||||
|
</AdminShell>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
@page "/login"
|
||||||
|
@page "/"
|
||||||
|
@attribute [AllowAnonymous]
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject HttpClient Http
|
||||||
|
|
||||||
|
<PageTitle>고객 포탈 로그인</PageTitle>
|
||||||
|
|
||||||
|
<div class="portal-login-container">
|
||||||
|
<div class="portal-login-card">
|
||||||
|
<h1>고객 포탈</h1>
|
||||||
|
<p>이메일과 비밀번호로 로그인하세요</p>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@ErrorMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form @onsubmit="HandleLogin">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="email" class="form-label">이메일</label>
|
||||||
|
<input type="email" class="form-control" id="email"
|
||||||
|
@bind="Email" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="password" class="form-label">비밀번호</label>
|
||||||
|
<input type="password" class="form-control" id="password"
|
||||||
|
@bind="Password" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100" disabled="@IsLoading">
|
||||||
|
@if (IsLoading) { <span>로그인 중...</span> } else { <span>로그인</span> }
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p class="text-muted">계정이 없으신가요? <a href="/register">가입하기</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<div class="oauth-section">
|
||||||
|
<p class="text-muted mb-3">또는 소셜 계정으로 로그인</p>
|
||||||
|
<button type="button" class="btn btn-outline-secondary w-100 mb-2" @onclick="LoginWithGoogle">
|
||||||
|
Google로 로그인
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary w-100 mb-2" @onclick="LoginWithNaver">
|
||||||
|
Naver로 로그인
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary w-100" @onclick="LoginWithKakao">
|
||||||
|
Kakao로 로그인
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.portal-login-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-login-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-login-card h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-login-card p {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string Email = "";
|
||||||
|
private string Password = "";
|
||||||
|
private string? ErrorMessage;
|
||||||
|
private bool IsLoading = false;
|
||||||
|
|
||||||
|
private async Task HandleLogin()
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
ErrorMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loginRequest = new { email = Email, password = Password };
|
||||||
|
var response = await Http.PostAsJsonAsync("/api/portal/login", loginRequest);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/dashboard", forceLoad: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorMessage = "로그인 정보를 확인할 수 없습니다.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ErrorMessage = "로그인 중 오류가 발생했습니다.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoginWithGoogle() => Navigation.NavigateTo("/api/portal/login/google");
|
||||||
|
private void LoginWithNaver() => Navigation.NavigateTo("/api/portal/login/naver");
|
||||||
|
private void LoginWithKakao() => Navigation.NavigateTo("/api/portal/login/kakao");
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@page "/logout"
|
||||||
|
@attribute [Authorize]
|
||||||
|
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject HttpClient Http
|
||||||
|
|
||||||
|
<PageTitle>로그아웃 중...</PageTitle>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: center; align-items: center; min-height: 100vh;">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<p>로그아웃 중입니다...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Http.PostAsync("/api/portal/logout", null);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 로그아웃 실패해도 무조건 로그인 페이지로
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/login", forceLoad: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@namespace TaxBaik.PortalClient.Components.Portal
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
|
||||||
|
<Router AppAssembly="@typeof(TaxBaik.PortalClient._Imports).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(TaxBaik.PortalClient.Components.Portal.Layout.MainLayout)">
|
||||||
|
<NotAuthorized>
|
||||||
|
<RedirectToLogin />
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeRouteView>
|
||||||
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<PageTitle>찾을 수 없음</PageTitle>
|
||||||
|
<LayoutView Layout="@typeof(TaxBaik.PortalClient.Components.Portal.Layout.MainLayout)">
|
||||||
|
<p>요청한 페이지를 찾을 수 없습니다.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
|
||||||
|
namespace TaxBaik.PortalClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Portal 사용자 인증 상태 (쿠키 기반)
|
||||||
|
/// 서버에서 PortalAuthDefaults.AuthenticationScheme으로 설정한 쿠키를 읽음
|
||||||
|
/// </summary>
|
||||||
|
public class PortalAuthenticationStateProvider : AuthenticationStateProvider
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public PortalAuthenticationStateProvider(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 서버의 쿠키 인증 상태 확인 엔드포인트
|
||||||
|
var response = await _httpClient.GetAsync("/api/portal/auth/me");
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
// TODO: JSON 파싱 후 ClaimsIdentity 생성
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, "user-id"),
|
||||||
|
new Claim(ClaimTypes.Name, "User Name")
|
||||||
|
};
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, "Portal");
|
||||||
|
var user = new ClaimsPrincipal(identity);
|
||||||
|
return new AuthenticationState(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 인증 상태 확인 실패 → 미인증 상태
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AuthenticationState(new ClaimsPrincipal());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using MudBlazor.Services;
|
||||||
|
using TaxBaik.PortalClient;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
|
builder.Services.AddAuthorizationCore();
|
||||||
|
|
||||||
|
// Portal 인증 (쿠키 기반)
|
||||||
|
builder.Services.AddScoped<AuthenticationStateProvider, PortalAuthenticationStateProvider>();
|
||||||
|
|
||||||
|
// HTTP 클라이언트 (쿠키 자동 포함)
|
||||||
|
builder.Services.AddScoped(sp =>
|
||||||
|
new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||||
|
});
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>TaxBaik.PortalClient</RootNamespace>
|
||||||
|
<!-- Portal SPA는 /portal 경로에서 호스팅 -->
|
||||||
|
<StaticWebAssetBasePath>portal</StaticWebAssetBasePath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||||
|
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using MudBlazor
|
||||||
|
@using TaxBaik.PortalClient
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>TaxBaik - 고객 포탈</title>
|
||||||
|
<!-- Portal SPA는 /portal/ 경로에서 호스팅 -->
|
||||||
|
<base href="/portal/" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="stylesheet" href="/css/admin.css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body class="blazor-dark">
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<!-- Standalone Blazor WebAssembly -->
|
||||||
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -385,12 +385,15 @@ app.MapHealthChecks("/healthz");
|
|||||||
app.MapRazorPages();
|
app.MapRazorPages();
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
// Admin Blazor WebAssembly SPA 호스팅 (/admin)
|
// Admin & Portal Blazor WebAssembly SPA 호스팅
|
||||||
app.UseBlazorFrameworkFiles("/admin");
|
app.UseBlazorFrameworkFiles("/admin");
|
||||||
app.UseStaticFiles("/admin");
|
app.UseStaticFiles("/admin");
|
||||||
|
app.UseBlazorFrameworkFiles("/portal");
|
||||||
|
app.UseStaticFiles("/portal");
|
||||||
|
|
||||||
// /admin 라우팅 폴백 (SPA 라우트 처리)
|
// SPA 라우팅 폴백 (각 경로에서 index.html 제공)
|
||||||
app.MapFallbackToFile("admin/{*path:nonfile}", "admin/index.html");
|
app.MapFallbackToFile("admin/{*path:nonfile}", "admin/index.html");
|
||||||
|
app.MapFallbackToFile("portal/{*path:nonfile}", "portal/index.html");
|
||||||
|
|
||||||
// 애플리케이션 시작/종료 로깅
|
// 애플리케이션 시작/종료 로깅
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||||
<ProjectReference Include="..\TaxBaik.Infrastructure\TaxBaik.Infrastructure.csproj" />
|
<ProjectReference Include="..\TaxBaik.Infrastructure\TaxBaik.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\TaxBaik.Web.Client\TaxBaik.Web.Client.csproj" />
|
<ProjectReference Include="..\TaxBaik.Web.Client\TaxBaik.Web.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\TaxBaik.Portal.Client\TaxBaik.Portal.Client.csproj" />
|
||||||
<!-- WebAssembly 번들 생성에 필수 -->
|
<!-- WebAssembly 번들 생성에 필수 -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user