diff --git a/TaxBaik.Web.Client/GlobalUsings.cs b/TaxBaik.Web.Client/GlobalUsings.cs
new file mode 100644
index 0000000..9a55fd3
--- /dev/null
+++ b/TaxBaik.Web.Client/GlobalUsings.cs
@@ -0,0 +1,2 @@
+global using System.Net.Http;
+global using System.Net.Http.Json;
diff --git a/TaxBaik.Web.Client/Pages/WasmPing.razor b/TaxBaik.Web.Client/Pages/WasmPing.razor
new file mode 100644
index 0000000..aeafb41
--- /dev/null
+++ b/TaxBaik.Web.Client/Pages/WasmPing.razor
@@ -0,0 +1,13 @@
+@* WASM 기반(M3) 검증용 컴포넌트. 라우팅/렌더모드 전면 적용은 M4에서 처리한다. *@
+@rendermode InteractiveWebAssembly
+
+
+ WebAssembly 렌더 모드 점검
+ 이 컴포넌트가 클릭에 반응하면 Interactive WebAssembly 기반이 정상 동작하는 것입니다.
+ 카운트: @count
+
+
+@code {
+ private int count;
+ private void Increment() => count++;
+}
diff --git a/TaxBaik.Web.Client/Program.cs b/TaxBaik.Web.Client/Program.cs
new file mode 100644
index 0000000..1bd5678
--- /dev/null
+++ b/TaxBaik.Web.Client/Program.cs
@@ -0,0 +1,51 @@
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using MudBlazor.Services;
+using TaxBaik.Application.Services;
+using TaxBaik.Web.Services;
+using TaxBaik.Web.Services.AdminClients;
+
+var builder = WebAssemblyHostBuilder.CreateDefault(args);
+
+// MudBlazor (WASM 측 인터랙티브 컴포넌트용)
+builder.Services.AddMudServices(config =>
+{
+ config.SnackbarConfiguration.HideTransitionDuration = 400;
+ config.SnackbarConfiguration.ShowTransitionDuration = 300;
+ config.PopoverOptions.ThrowOnDuplicateProvider = false;
+});
+
+// API Base Url 동적 구성 (호스트 기준 /taxbaik/api/)
+var apiBaseUrl = builder.HostEnvironment.BaseAddress.TrimEnd('/') + "/taxbaik/api/";
+
+// HTTP Client for API (with automatic token refresh)
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+builder.Services.AddHttpClient(client =>
+{
+ client.BaseAddress = new Uri(apiBaseUrl);
+}).AddHttpMessageHandler();
+
+// 각 Browser API Client 등록
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler();
+
+// Blazor 인증 (WASM 측 클라이언트)
+builder.Services.AddScoped();
+builder.Services.AddScoped(sp => sp.GetRequiredService());
+builder.Services.AddScoped();
+builder.Services.AddCascadingAuthenticationState();
+builder.Services.AddAuthorizationCore();
+
+await builder.Build().RunAsync();
diff --git a/TaxBaik.Web/Services/AdminClients/ICommonCodeBrowserClient.cs b/TaxBaik.Web.Client/Services/AdminClients/ICommonCodeBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminClients/ICommonCodeBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AdminClients/ICommonCodeBrowserClient.cs
diff --git a/TaxBaik.Web/Services/AdminClients/IConsultingActivityBrowserClient.cs b/TaxBaik.Web.Client/Services/AdminClients/IConsultingActivityBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminClients/IConsultingActivityBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AdminClients/IConsultingActivityBrowserClient.cs
diff --git a/TaxBaik.Web/Services/AdminClients/IContractBrowserClient.cs b/TaxBaik.Web.Client/Services/AdminClients/IContractBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminClients/IContractBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AdminClients/IContractBrowserClient.cs
diff --git a/TaxBaik.Web/Services/AdminClients/IRevenueTrackingBrowserClient.cs b/TaxBaik.Web.Client/Services/AdminClients/IRevenueTrackingBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminClients/IRevenueTrackingBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AdminClients/IRevenueTrackingBrowserClient.cs
diff --git a/TaxBaik.Web/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs b/TaxBaik.Web.Client/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs
diff --git a/TaxBaik.Web/Services/AdminClients/ITaxProfileBrowserClient.cs b/TaxBaik.Web.Client/Services/AdminClients/ITaxProfileBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminClients/ITaxProfileBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AdminClients/ITaxProfileBrowserClient.cs
diff --git a/TaxBaik.Web/Services/AdminDashboardClient.cs b/TaxBaik.Web.Client/Services/AdminDashboardClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AdminDashboardClient.cs
rename to TaxBaik.Web.Client/Services/AdminDashboardClient.cs
diff --git a/TaxBaik.Web/Services/AnnouncementBrowserClient.cs b/TaxBaik.Web.Client/Services/AnnouncementBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/AnnouncementBrowserClient.cs
rename to TaxBaik.Web.Client/Services/AnnouncementBrowserClient.cs
diff --git a/TaxBaik.Web/Services/ApiClient.cs b/TaxBaik.Web.Client/Services/ApiClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/ApiClient.cs
rename to TaxBaik.Web.Client/Services/ApiClient.cs
diff --git a/TaxBaik.Web/Services/ClientBrowserClient.cs b/TaxBaik.Web.Client/Services/ClientBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/ClientBrowserClient.cs
rename to TaxBaik.Web.Client/Services/ClientBrowserClient.cs
diff --git a/TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs b/TaxBaik.Web.Client/Services/CustomAuthenticationStateProvider.cs
similarity index 81%
rename from TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs
rename to TaxBaik.Web.Client/Services/CustomAuthenticationStateProvider.cs
index 4ca3413..e46b81f 100644
--- a/TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs
+++ b/TaxBaik.Web.Client/Services/CustomAuthenticationStateProvider.cs
@@ -1,6 +1,7 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
+using TaxBaik.Application.Services;
namespace TaxBaik.Web.Services;
@@ -8,18 +9,18 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorage;
private readonly ITokenStore _tokenStore;
- private readonly AuthService _authService;
+ private readonly IApiClient _apiClient;
private readonly ILogger _logger;
public CustomAuthenticationStateProvider(
ILocalStorageService localStorage,
ITokenStore tokenStore,
- AuthService authService,
+ IApiClient apiClient,
ILogger logger)
{
_localStorage = localStorage;
_tokenStore = tokenStore;
- _authService = authService;
+ _apiClient = apiClient;
_logger = logger;
}
@@ -64,8 +65,9 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
if (!string.IsNullOrEmpty(_tokenStore.RefreshToken) && ShouldRefreshToken())
{
_logger.LogInformation("토큰 만료 5분 전 - 자동 갱신 시작");
- var newTokenPair = await _authService.RefreshAccessTokenAsync(_tokenStore.RefreshToken);
- if (newTokenPair != null)
+ var request = new { RefreshToken = _tokenStore.RefreshToken };
+ var newTokenPair = await _apiClient.PostAsync("auth/refresh", request);
+ if (newTokenPair != null && !string.IsNullOrEmpty(newTokenPair.AccessToken))
{
await LoginAsync(newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.ExpiresIn);
_logger.LogInformation("토큰 자동 갱신 성공");
@@ -79,7 +81,7 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
}
}
- var principal = _authService.ValidateToken(accessToken ?? string.Empty);
+ var principal = ValidateTokenWithoutDb(accessToken ?? string.Empty);
if (principal == null)
{
await LogoutAsync();
@@ -95,6 +97,22 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
}
}
+ private ClaimsPrincipal? ValidateTokenWithoutDb(string token)
+ {
+ try
+ {
+ var handler = new JwtSecurityTokenHandler();
+ var jwtToken = handler.ReadJwtToken(token);
+ var identity = new ClaimsIdentity(jwtToken.Claims, "jwt");
+ return new ClaimsPrincipal(identity);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+
public async Task LoginAsync(string accessToken, string refreshToken, int expiresIn)
{
var tokenExpiryTicks = DateTime.UtcNow.AddSeconds(expiresIn).Ticks;
@@ -158,3 +176,17 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
}
}
}
+
+public class WasmAuthTokenPair
+{
+ public WasmAuthTokenPair() { }
+ public WasmAuthTokenPair(string accessToken, string refreshToken, int expiresIn)
+ {
+ AccessToken = accessToken;
+ RefreshToken = refreshToken;
+ ExpiresIn = expiresIn;
+ }
+ public string AccessToken { get; set; } = "";
+ public string RefreshToken { get; set; } = "";
+ public int ExpiresIn { get; set; }
+}
diff --git a/TaxBaik.Web/Services/FaqBrowserClient.cs b/TaxBaik.Web.Client/Services/FaqBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/FaqBrowserClient.cs
rename to TaxBaik.Web.Client/Services/FaqBrowserClient.cs
diff --git a/TaxBaik.Web/Services/ILocalStorageService.cs b/TaxBaik.Web.Client/Services/ILocalStorageService.cs
similarity index 100%
rename from TaxBaik.Web/Services/ILocalStorageService.cs
rename to TaxBaik.Web.Client/Services/ILocalStorageService.cs
diff --git a/TaxBaik.Web/Services/ITokenStore.cs b/TaxBaik.Web.Client/Services/ITokenStore.cs
similarity index 100%
rename from TaxBaik.Web/Services/ITokenStore.cs
rename to TaxBaik.Web.Client/Services/ITokenStore.cs
diff --git a/TaxBaik.Web/Services/InquiryBrowserClient.cs b/TaxBaik.Web.Client/Services/InquiryBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/InquiryBrowserClient.cs
rename to TaxBaik.Web.Client/Services/InquiryBrowserClient.cs
diff --git a/TaxBaik.Web/Services/LocalStorageService.cs b/TaxBaik.Web.Client/Services/LocalStorageService.cs
similarity index 100%
rename from TaxBaik.Web/Services/LocalStorageService.cs
rename to TaxBaik.Web.Client/Services/LocalStorageService.cs
diff --git a/TaxBaik.Web/Services/TaxFilingBrowserClient.cs b/TaxBaik.Web.Client/Services/TaxFilingBrowserClient.cs
similarity index 100%
rename from TaxBaik.Web/Services/TaxFilingBrowserClient.cs
rename to TaxBaik.Web.Client/Services/TaxFilingBrowserClient.cs
diff --git a/TaxBaik.Web/Services/TokenRefreshHandler.cs b/TaxBaik.Web.Client/Services/TokenRefreshHandler.cs
similarity index 94%
rename from TaxBaik.Web/Services/TokenRefreshHandler.cs
rename to TaxBaik.Web.Client/Services/TokenRefreshHandler.cs
index 65cb762..9c5049d 100644
--- a/TaxBaik.Web/Services/TokenRefreshHandler.cs
+++ b/TaxBaik.Web.Client/Services/TokenRefreshHandler.cs
@@ -62,7 +62,7 @@ public class TokenRefreshHandler : DelegatingHandler
return response;
}
- private async Task RefreshTokenAsync(string refreshToken, HttpRequestMessage originalRequest, CancellationToken ct)
+ private async Task RefreshTokenAsync(string refreshToken, HttpRequestMessage originalRequest, CancellationToken ct)
{
try
{
@@ -87,7 +87,7 @@ public class TokenRefreshHandler : DelegatingHandler
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return result != null
- ? new AuthTokenPair(result.AccessToken, result.RefreshToken, result.ExpiresIn)
+ ? new WasmAuthTokenPair(result.AccessToken, result.RefreshToken, result.ExpiresIn)
: null;
}
catch (Exception ex)
diff --git a/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj b/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj
new file mode 100644
index 0000000..66509f4
--- /dev/null
+++ b/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+ TaxBaik.WasmClient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TaxBaik.Web.Client/_Imports.razor b/TaxBaik.Web.Client/_Imports.razor
new file mode 100644
index 0000000..aa7052b
--- /dev/null
+++ b/TaxBaik.Web.Client/_Imports.razor
@@ -0,0 +1,13 @@
+@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.AspNetCore.Components.WebAssembly.Http
+@using Microsoft.JSInterop
+@using MudBlazor
+@using TaxBaik.WasmClient
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
diff --git a/TaxBaik.Web/Components/Admin/App.razor b/TaxBaik.Web/Components/Admin/App.razor
index 5ab6a4c..7cca9f5 100644
--- a/TaxBaik.Web/Components/Admin/App.razor
+++ b/TaxBaik.Web/Components/Admin/App.razor
@@ -32,7 +32,7 @@
-
+
diff --git a/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor b/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor
index 025a100..5a890b0 100644
--- a/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor
+++ b/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor
@@ -2,6 +2,7 @@
@inject NavigationManager Navigation
@inject IJSRuntime JS
@implements IDisposable
+@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor
index d91a271..2cb4c20 100644
--- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor
+++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor
@@ -1,5 +1,6 @@
@page "/admin/blog/create"
@attribute [Authorize]
+@rendermode @(new InteractiveServerRenderMode(prerender: false))
@using TaxBaik.Application.DTOs
@using TaxBaik.Application.Services
@using TaxBaik.Domain.Interfaces
diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor
index 0e51b92..2633efb 100644
--- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor
+++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor
@@ -1,5 +1,6 @@
@page "/admin/blog/{id:int}/edit"
@attribute [Authorize]
+@rendermode @(new InteractiveServerRenderMode(prerender: false))
@using TaxBaik.Application.DTOs
@using TaxBaik.Application.Services
@using TaxBaik.Domain.Interfaces
diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs
index 33d52b2..cfc0922 100644
--- a/TaxBaik.Web/Program.cs
+++ b/TaxBaik.Web/Program.cs
@@ -54,7 +54,9 @@ builder.Services.AddHealthChecks();
// Razor Pages + Blazor Server 통합
builder.Services.AddRazorPages();
-builder.Services.AddRazorComponents().AddInteractiveServerComponents();
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents()
+ .AddInteractiveWebAssemblyComponents();
builder.Services.Configure(options =>
{
options.DetailedErrors = true;
@@ -354,6 +356,8 @@ app.MapRazorPages();
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
+ .AddInteractiveWebAssemblyRenderMode()
+ .AddAdditionalAssemblies(typeof(TaxBaik.WasmClient._Imports).Assembly)
.AllowAnonymous();
// 애플리케이션 시작/종료 로깅
diff --git a/TaxBaik.Web/TaxBaik.Web.csproj b/TaxBaik.Web/TaxBaik.Web.csproj
index f14e1ea..0af16c5 100644
--- a/TaxBaik.Web/TaxBaik.Web.csproj
+++ b/TaxBaik.Web/TaxBaik.Web.csproj
@@ -3,6 +3,7 @@
+
@@ -12,6 +13,7 @@
+
diff --git a/TaxBaik.sln b/TaxBaik.sln
index 10b5d07..5631854 100644
--- a/TaxBaik.sln
+++ b/TaxBaik.sln
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Web", "TaxBaik.Web\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Application.Tests", "TaxBaik.Application.Tests\TaxBaik.Application.Tests.csproj", "{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Web.Client", "TaxBaik.Web.Client\TaxBaik.Web.Client.csproj", "{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -83,6 +85,18 @@ Global
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x64.Build.0 = Release|Any CPU
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x86.ActiveCfg = Release|Any CPU
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x86.Build.0 = Release|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x64.Build.0 = Debug|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x86.Build.0 = Debug|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x64.ActiveCfg = Release|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x64.Build.0 = Release|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x86.ActiveCfg = Release|Any CPU
+ {C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE