diff --git a/TaxBaik.Web/Components/Admin/App.razor b/TaxBaik.Web/Components/Admin/App.razor index fa08e71..6338cfc 100644 --- a/TaxBaik.Web/Components/Admin/App.razor +++ b/TaxBaik.Web/Components/Admin/App.razor @@ -13,8 +13,16 @@ +
+
+ 관리자 세션을 다시 연결하고 있습니다. + 배포 또는 서버 재시작 중이면 잠시 후 자동으로 새로고침됩니다. +
+
+ + diff --git a/TaxBaik.Web/Components/Admin/Pages/Login.razor b/TaxBaik.Web/Components/Admin/Pages/Login.razor index ee6c64a..5a06fdd 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Login.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Login.razor @@ -80,7 +80,7 @@ await ApiClient.SetAuthToken(response.Token); await AuthStateProvider.LoginAsync(response.Token); - NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: false); + NavigationManager.NavigateTo(GetReturnUrl(), forceLoad: false); } catch { @@ -100,4 +100,22 @@ public string Username { get; set; } = ""; public string Password { get; set; } = ""; } + + private string GetReturnUrl() + { + var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); + if (!Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var returnUrl) + || string.IsNullOrWhiteSpace(returnUrl)) + { + return "/taxbaik/admin/dashboard"; + } + + var value = returnUrl.ToString(); + if (!value.StartsWith("admin", StringComparison.OrdinalIgnoreCase)) + { + return "/taxbaik/admin/dashboard"; + } + + return $"/taxbaik/{value.TrimStart('/')}"; + } } diff --git a/TaxBaik.Web/Components/Admin/RedirectToLogin.razor b/TaxBaik.Web/Components/Admin/RedirectToLogin.razor index 90ea4b2..df1a791 100644 --- a/TaxBaik.Web/Components/Admin/RedirectToLogin.razor +++ b/TaxBaik.Web/Components/Admin/RedirectToLogin.razor @@ -1,8 +1,13 @@ @inject NavigationManager Navigation +@inject IJSRuntime Js @code { - protected override void OnInitialized() + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (!firstRender) + return; + + await Js.InvokeVoidAsync("taxbaikAdminSession.clearAuthToken"); var returnUrl = Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri)); Navigation.NavigateTo($"/taxbaik/admin/login?returnUrl={returnUrl}", replace: true); } diff --git a/TaxBaik.Web/Components/Admin/_Imports.razor b/TaxBaik.Web/Components/Admin/_Imports.razor index 3bda42d..c6fbec2 100644 --- a/TaxBaik.Web/Components/Admin/_Imports.razor +++ b/TaxBaik.Web/Components/Admin/_Imports.razor @@ -6,6 +6,7 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Authorization +@using Microsoft.JSInterop @using MudBlazor @using TaxBaik.Web.Services @using TaxBaik.Domain.Entities diff --git a/TaxBaik.Web/wwwroot/css/admin.css b/TaxBaik.Web/wwwroot/css/admin.css index e2f6a93..9626e50 100644 --- a/TaxBaik.Web/wwwroot/css/admin.css +++ b/TaxBaik.Web/wwwroot/css/admin.css @@ -169,6 +169,41 @@ button:disabled { opacity: 0.6; } +.admin-reconnect-modal { + display: none; +} + +.admin-reconnect-modal.components-reconnect-show, +.admin-reconnect-modal.components-reconnect-failed, +.admin-reconnect-modal.components-reconnect-rejected { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(15, 23, 42, 0.48); +} + +.admin-reconnect-card { + width: min(420px, 100%); + padding: 24px; + border-radius: 16px; + background: #fff; + box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28); +} + +.admin-reconnect-card strong, +.admin-reconnect-card span { + display: block; +} + +.admin-reconnect-card span { + margin-top: 8px; + color: #64748b; +} + /* Responsive */ @media (max-width: 600px) { .mud-container-maxwidth-small { diff --git a/TaxBaik.Web/wwwroot/js/admin-session.js b/TaxBaik.Web/wwwroot/js/admin-session.js new file mode 100644 index 0000000..b4a534a --- /dev/null +++ b/TaxBaik.Web/wwwroot/js/admin-session.js @@ -0,0 +1,26 @@ +window.taxbaikAdminSession = { + clearAuthToken: function () { + try { + localStorage.removeItem('auth_token'); + } catch { + // Ignore storage errors; redirect still recovers the session. + } + }, + watchReconnect: function () { + const modal = document.getElementById('components-reconnect-modal'); + if (!modal) { + return; + } + + const reloadOnRejectedCircuit = () => { + const className = modal.className || ''; + if (className.includes('components-reconnect-failed') || + className.includes('components-reconnect-rejected')) { + window.setTimeout(() => window.location.reload(), 1500); + } + }; + + new MutationObserver(reloadOnRejectedCircuit) + .observe(modal, { attributes: true, attributeFilter: ['class'] }); + } +};