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