diff --git a/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor b/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor
index 1524f6c..fe0710b 100644
--- a/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor
+++ b/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor
@@ -1,6 +1,6 @@
@page "/admin/login"
@layout TaxBaik.WasmClient.Components.Admin.Layout.BlankLayout
@attribute [AllowAnonymous]
-@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: true))
+@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
로그인
-
+
diff --git a/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginFormNative.razor b/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginFormNative.razor
new file mode 100644
index 0000000..07d40b6
--- /dev/null
+++ b/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginFormNative.razor
@@ -0,0 +1,141 @@
+@using System.Text.Json
+@inject ILocalStorageService LocalStorageService
+@inject NavigationManager Navigation
+@inject ISnackbar Snackbar
+@inject HttpClient Http
+@inject AuthenticationStateProvider AuthStateProvider
+
+
+
+ 관리자 로그인
+
+
+
+
+
+
+
+
+
+
+
+ @(isLoggingIn ? "로그인 중..." : "로그인")
+
+
+
+
+
+@code {
+ private MudForm? form;
+ private string username = "";
+ private string password = "";
+ private bool rememberMe = false;
+ private bool isLoggingIn = false;
+ private const string RememberedUsernameKey = "admin-remembered-username";
+ private const string RememberedCheckboxKey = "admin-remember-checkbox";
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ username = await LocalStorageService.GetItemAsStringAsync(RememberedUsernameKey) ?? "";
+ var checkboxValue = await LocalStorageService.GetItemAsStringAsync(RememberedCheckboxKey) ?? "false";
+ rememberMe = checkboxValue == "true" && !string.IsNullOrEmpty(username);
+ }
+ catch
+ {
+ username = "";
+ rememberMe = false;
+ }
+ }
+
+ private async Task HandleLogin()
+ {
+ if (form == null || isLoggingIn) return;
+
+ await form.Validate();
+ if (!form.IsValid) return;
+
+ isLoggingIn = true;
+ try
+ {
+ var response = await Http.PostAsJsonAsync("/api/auth/login", new { username, password });
+ if (!response.IsSuccessStatusCode)
+ {
+ Snackbar.Add("로그인 실패: 사용자명 또는 비밀번호가 올바르지 않습니다", Severity.Error);
+ return;
+ }
+
+ var json = await response.Content.ReadAsStringAsync();
+ var loginResponse = JsonSerializer.Deserialize(json);
+ if (loginResponse == null || string.IsNullOrEmpty(loginResponse.AccessToken))
+ {
+ Snackbar.Add("로그인 실패: 응답 오류", Severity.Error);
+ return;
+ }
+
+ // 로컬 저장소 저장
+ await LocalStorageService.SetItemAsStringAsync("accessToken", loginResponse.AccessToken);
+ await LocalStorageService.SetItemAsStringAsync("refreshToken", loginResponse.RefreshToken ?? "");
+ var expiryTicks = DateTimeOffset.UtcNow.AddSeconds(loginResponse.ExpiresIn).UtcTicks;
+ await LocalStorageService.SetItemAsStringAsync("tokenExpiry", expiryTicks.ToString());
+
+ // 아이디 저장
+ if (rememberMe)
+ {
+ await LocalStorageService.SetItemAsStringAsync(RememberedUsernameKey, username);
+ await LocalStorageService.SetItemAsStringAsync(RememberedCheckboxKey, "true");
+ }
+ else
+ {
+ await LocalStorageService.RemoveItemAsync(RememberedUsernameKey);
+ await LocalStorageService.RemoveItemAsync(RememberedCheckboxKey);
+ }
+
+ // Blazor 인증 상태 업데이트 (직접 호출)
+ if (AuthStateProvider is CustomAuthenticationStateProvider customProvider)
+ {
+ await customProvider.LoginAsync(loginResponse.AccessToken, loginResponse.RefreshToken ?? "", loginResponse.ExpiresIn);
+ }
+
+ // 대시보드로 이동
+ Navigation.NavigateTo("/admin/dashboard", replace: true);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"로그인 중 오류: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ isLoggingIn = false;
+ }
+ }
+
+ private class LoginResponse
+ {
+ public string AccessToken { get; set; } = "";
+ public string RefreshToken { get; set; } = "";
+ public int ExpiresIn { get; set; }
+ }
+}
diff --git a/tests/e2e/admin-login.spec.ts b/tests/e2e/admin-login.spec.ts
index 402ce2c..b76969e 100644
--- a/tests/e2e/admin-login.spec.ts
+++ b/tests/e2e/admin-login.spec.ts
@@ -9,6 +9,8 @@ test.describe('admin authentication', () => {
test.skip(!password, 'E2E_ADMIN_PASSWORD is required.');
const consoleErrors: string[] = [];
+ const networkRequests: { url: string; status?: number }[] = [];
+
page.on('console', message => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
@@ -17,16 +19,60 @@ test.describe('admin authentication', () => {
page.on('pageerror', error => {
consoleErrors.push(error.message);
});
+ page.on('response', response => {
+ if (response.url().includes('/api/auth/login') || response.url().includes('/admin/')) {
+ networkRequests.push({ url: response.url(), status: response.status() });
+ }
+ });
await page.goto(`${baseUrl}/admin/login`);
await expect(page.locator('input[placeholder="사용자명"]')).toBeVisible();
await expect(page.locator('input[placeholder="비밀번호"]')).toBeVisible();
- await page.locator('input[placeholder="사용자명"]').fill(username);
- await page.locator('input[placeholder="비밀번호"]').fill(password);
- await page.getByRole('button', { name: '로그인' }).click();
+ // Wait for form to be fully ready
+ await page.waitForTimeout(500);
- await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/);
+ const usernameInput = page.locator('input[placeholder="사용자명"]');
+ const passwordInput = page.locator('input[placeholder="비밀번호"]');
+ const loginButton = page.getByRole('button', { name: '로그인' });
+
+ await usernameInput.fill(username);
+ await passwordInput.fill(password);
+
+ // Wait for inputs to register
+ await page.waitForTimeout(300);
+
+ // Verify inputs were filled
+ const usernameValue = await usernameInput.inputValue();
+ const passwordValue = await passwordInput.inputValue();
+ console.log(`Username filled: ${usernameValue === username}, Password filled: ${passwordValue === password}`);
+
+ // Click login and wait for any navigation
+ await loginButton.click();
+
+ // Wait a bit for form submission + page reload
+ await page.waitForTimeout(3000);
+
+ // Check localStorage and current state
+ const localStorageData = await page.evaluate(() => ({
+ accessToken: localStorage.getItem('accessToken'),
+ refreshToken: localStorage.getItem('refreshToken'),
+ tokenExpiry: localStorage.getItem('tokenExpiry')
+ }));
+
+ console.log(`localStorage has accessToken: ${!!localStorageData.accessToken}`);
+ console.log(`Current URL after login: ${page.url()}`);
+ console.log(`Network requests: ${JSON.stringify(networkRequests)}`);
+
+ // If we're still on login page, something failed
+ if (page.url().includes('/admin/login')) {
+ console.log('Still on login page after 3 seconds!');
+ // Try to find error message
+ const errorMsg = await page.locator('.login-error-message').textContent().catch(() => null);
+ console.log(`Error message: ${errorMsg}`);
+ }
+
+ await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/, { timeout: 20_000 });
await expect(page.getByRole('heading', { name: '대시보드' }).first()).toBeVisible({ timeout: 20_000 });
await expect(page.getByRole('link', { name: /로그아웃/ })).toBeVisible();
expect(consoleErrors, 'browser console/page errors').toEqual([]);