diff --git a/src/dotnet/QuantEngine.Web/Client/Layout/AuthLayout.razor b/src/dotnet/QuantEngine.Web/Client/Layout/AuthLayout.razor index 45fe096..5a11cf8 100644 --- a/src/dotnet/QuantEngine.Web/Client/Layout/AuthLayout.razor +++ b/src/dotnet/QuantEngine.Web/Client/Layout/AuthLayout.razor @@ -1,75 +1,20 @@ @inherits LayoutComponentBase -@using QuantEngine.Web.Client.Theme @rendermode InteractiveWebAssembly - - - - - + - - - -
- - -
- - - QuantEngine - -
-
- - -
- @Body -
- - - -
- +@Body @code { - private MudTheme _theme = AppTheme.LightTheme; } diff --git a/src/dotnet/QuantEngine.Web/Components/App.razor b/src/dotnet/QuantEngine.Web/Components/App.razor index f600a15..7f8eaf0 100644 --- a/src/dotnet/QuantEngine.Web/Components/App.razor +++ b/src/dotnet/QuantEngine.Web/Components/App.razor @@ -21,7 +21,8 @@
- +
diff --git a/src/dotnet/QuantEngine.Web/Components/Layout/AuthLayout.razor b/src/dotnet/QuantEngine.Web/Components/Layout/AuthLayout.razor index 0e12905..76eddd4 100644 --- a/src/dotnet/QuantEngine.Web/Components/Layout/AuthLayout.razor +++ b/src/dotnet/QuantEngine.Web/Components/Layout/AuthLayout.razor @@ -1,46 +1,22 @@ @inherits LayoutComponentBase @using QuantEngine.Web.Client.Theme - - - - - - -
-
-
- @Body -
-
-
+ +@Body + @code { - private MudTheme _theme = AppTheme.LightTheme; } diff --git a/src/dotnet/QuantEngine.Web/Components/Pages/LoginSimple.razor b/src/dotnet/QuantEngine.Web/Components/Pages/LoginSimple.razor deleted file mode 100644 index c807382..0000000 --- a/src/dotnet/QuantEngine.Web/Components/Pages/LoginSimple.razor +++ /dev/null @@ -1,185 +0,0 @@ -@page "/login" -@attribute [AllowAnonymous] -@layout AuthLayout -@rendermode InteractiveServer -@inject HttpClient Http -@inject NavigationManager Nav - -로그인 - QuantEngine - - - - - -@code { - private string _username = string.Empty; - private string _password = string.Empty; - private string _errorMessage = string.Empty; - private bool _isSubmitting = false; - private bool _rememberUsername = true; - - private async Task LoginAsync() - { - _errorMessage = string.Empty; - - if (string.IsNullOrWhiteSpace(_username) || string.IsNullOrWhiteSpace(_password)) - { - _errorMessage = "아이디와 비밀번호를 모두 입력해 주세요."; - return; - } - - _isSubmitting = true; - StateHasChanged(); - - try - { - var response = await Http.PostAsJsonAsync("/api/auth/login", new - { - Username = _username, - Password = _password - }); - - if (response.IsSuccessStatusCode) - { - Nav.NavigateTo("/", forceLoad: true); - return; - } - - _errorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; - } - catch (Exception ex) - { - _errorMessage = $"오류: {ex.Message}"; - } - finally - { - _isSubmitting = false; - } - } -} diff --git a/src/dotnet/QuantEngine.Web/Pages/Login.cshtml b/src/dotnet/QuantEngine.Web/Pages/Login.cshtml new file mode 100644 index 0000000..0674146 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Pages/Login.cshtml @@ -0,0 +1,300 @@ +@page "/login" +@using QuantEngine.Web.Pages +@model QuantEngine.Web.Pages.LoginModel +@{ + ViewData["Title"] = "로그인 - QuantEngine"; +} + + + + + + + @ViewData["Title"] + + + + + + + + diff --git a/src/dotnet/QuantEngine.Web/Pages/Login.cshtml.cs b/src/dotnet/QuantEngine.Web/Pages/Login.cshtml.cs new file mode 100644 index 0000000..8fa713a --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Pages/Login.cshtml.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace QuantEngine.Web.Pages +{ + public class LoginModel : PageModel + { + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public string? Username { get; set; } + public bool RememberUsername { get; set; } + public string? ErrorMessage { get; set; } + public string? SuccessMessage { get; set; } + + public LoginModel(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void OnGet() + { + // GET 요청: 로그인 페이지 표시 + // 쿠키에서 아이디 복원 (선택사항) + if (Request.Cookies.TryGetValue("quant_admin_username", out var savedUsername)) + { + Username = savedUsername; + RememberUsername = true; + } + } + + public async Task OnPostAsync(string username, string password, bool rememberUsername) + { + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + { + ErrorMessage = "아이디와 비밀번호를 모두 입력해 주세요."; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + + try + { + // API 호출 + var loginRequest = new { Username = username, Password = password }; + var response = await _httpClient.PostAsJsonAsync("/api/auth/login", loginRequest); + + if (response.IsSuccessStatusCode) + { + // 성공: 쿠키에 아이디 저장 + if (rememberUsername) + { + Response.Cookies.Append( + "quant_admin_username", + username, + new Microsoft.AspNetCore.Http.CookieOptions + { + Expires = DateTimeOffset.UtcNow.AddDays(30), + HttpOnly = false, // JavaScript에서 접근 가능 + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict + } + ); + } + else + { + Response.Cookies.Delete("quant_admin_username"); + } + + // 홈페이지로 리다이렉트 + return RedirectToPage("/Index"); + } + else + { + ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "로그인 중 오류 발생"); + ErrorMessage = $"오류 발생: {ex.Message}"; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + } + } +} diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 3da3a8c..6082586 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -35,6 +35,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog(); // Add services to the container. +builder.Services.AddRazorPages(); builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); @@ -414,6 +415,12 @@ app.MapPost("/api/history/{domain}", async (string domain, JsonElement payload, }); }); +// Razor Pages (로그인 등) +app.MapRazorPages(); + +// 정적 로그인 페이지 (wwwroot/login.html) +app.MapGet("/login", () => Results.File("wwwroot/login.html", "text/html")); + app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() diff --git a/src/dotnet/QuantEngine.Web/wwwroot/login.html b/src/dotnet/QuantEngine.Web/wwwroot/login.html new file mode 100644 index 0000000..ccc9a41 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/wwwroot/login.html @@ -0,0 +1,316 @@ + + + + + + 로그인 - QuantEngine + + + + + + + + diff --git a/test-results/login-card-closeup.png b/test-results/login-card-closeup.png deleted file mode 100644 index 5914615..0000000 Binary files a/test-results/login-card-closeup.png and /dev/null differ diff --git a/test-results/login-final-validation.png b/test-results/login-final-validation.png new file mode 100644 index 0000000..51d3658 Binary files /dev/null and b/test-results/login-final-validation.png differ diff --git a/test-results/login-mobile-view.png b/test-results/login-mobile-view.png new file mode 100644 index 0000000..24735ba Binary files /dev/null and b/test-results/login-mobile-view.png differ diff --git a/test-results/login-page-full.png b/test-results/login-page-full.png deleted file mode 100644 index e27e623..0000000 Binary files a/test-results/login-page-full.png and /dev/null differ diff --git a/tests/e2e/html-inspection.spec.ts b/tests/e2e/html-inspection.spec.ts new file mode 100644 index 0000000..5178d3d --- /dev/null +++ b/tests/e2e/html-inspection.spec.ts @@ -0,0 +1,32 @@ +import { test } from '@playwright/test'; + +test('HTML 구조 검사', async ({ page }) => { + await page.goto('http://localhost:5265/login', { waitUntil: 'networkidle' }); + await page.waitForTimeout(3000); + + const html = await page.content(); + + // 중요한 요소 확인 + console.log('\n╔════════════════════════════════════════════════════╗'); + console.log('║ HTML 구조 검사 ║'); + console.log('╚════════════════════════════════════════════════════╝\n'); + + console.log('🔍 주요 요소 검사:'); + console.log(` MudPaper 있음: ${html.includes('mud-paper') ? '✅' : '❌'}`); + console.log(` MudTextField 있음: ${html.includes('mud-textfield') ? '✅' : '❌'}`); + console.log(` login-container 있음: ${html.includes('login-container') ? '✅' : '❌'}`); + console.log(` login-card 있음: ${html.includes('login-card') ? '✅' : '❌'}`); + console.log(` form-input 있음: ${html.includes('form-input') ? '✅' : '❌'}`); + + console.log('\n📊 input 태그 개수:', (html.match(/]*>([\s\S]*?)<\/body>/i); + if (bodyMatch) { + const bodyContent = bodyMatch[1]; + // 처음 500자만 출력 (정리된 형태) + const cleaned = bodyContent.replace(/\s+/g, ' ').substring(0, 600); + console.log(cleaned); + } +}); diff --git a/tests/e2e/login-screenshot-final.spec.ts b/tests/e2e/login-screenshot-final.spec.ts new file mode 100644 index 0000000..0a58dfe --- /dev/null +++ b/tests/e2e/login-screenshot-final.spec.ts @@ -0,0 +1,106 @@ +import { test } from '@playwright/test'; + +test('로그인 화면 최종 스크린샷 - 스타일 검증', async ({ page }) => { + console.log('\n╔════════════════════════════════════════════════════╗'); + console.log('║ 로그인 화면 스타일 최종 검증 ║'); + console.log('╚════════════════════════════════════════════════════╝\n'); + + // 1️⃣ 페이지 로드 + console.log('1️⃣ 로그인 페이지 로드...'); + await page.goto('http://localhost:5265/login', { waitUntil: 'networkidle' }); + await page.waitForTimeout(3000); + console.log(' ✓ 페이지 로드 완료'); + + // 2️⃣ 계산된 스타일 검증 + console.log('\n2️⃣ 계산된 스타일 검증...'); + + const bodyStyle = await page.evaluate(() => { + const body = document.body; + return { + backgroundColor: window.getComputedStyle(body).backgroundColor, + color: window.getComputedStyle(body).color, + fontFamily: window.getComputedStyle(body).fontFamily + }; + }); + + console.log(` Body Background: ${bodyStyle.backgroundColor}`); + console.log(` Body Text Color: ${bodyStyle.color}`); + console.log(` Font Family: ${bodyStyle.fontFamily}`); + + // 3️⃣ 로그인 카드 스타일 + console.log('\n3️⃣ 로그인 카드 스타일...'); + + const cardStyle = await page.locator('.login-card').evaluate((el: any) => ({ + display: window.getComputedStyle(el).display, + background: window.getComputedStyle(el).backgroundColor, + border: window.getComputedStyle(el).border, + borderRadius: window.getComputedStyle(el).borderRadius, + padding: window.getComputedStyle(el).padding, + width: window.getComputedStyle(el).width + })); + + console.log(` Display: ${cardStyle.display}`); + console.log(` Background: ${cardStyle.background}`); + console.log(` Border: ${cardStyle.border}`); + console.log(` Border Radius: ${cardStyle.borderRadius}`); + console.log(` Width: ${cardStyle.width}`); + + // 4️⃣ 입력 필드 스타일 + console.log('\n4️⃣ 입력 필드 스타일...'); + + const inputStyle = await page.locator('input[type="text"], input[type="password"]').first().evaluate((el: any) => ({ + display: window.getComputedStyle(el).display, + backgroundColor: window.getComputedStyle(el).backgroundColor, + color: window.getComputedStyle(el).color, + border: window.getComputedStyle(el).border, + padding: window.getComputedStyle(el).padding, + fontSize: window.getComputedStyle(el).fontSize + })); + + console.log(` Display: ${inputStyle.display}`); + console.log(` Background: ${inputStyle.backgroundColor}`); + console.log(` Text Color: ${inputStyle.color}`); + console.log(` Border: ${inputStyle.border}`); + console.log(` Padding: ${inputStyle.padding}`); + console.log(` Font Size: ${inputStyle.fontSize}`); + + // 5️⃣ 로그인 버튼 스타일 + console.log('\n5️⃣ 로그인 버튼 스타일...'); + + const buttonStyle = await page.locator('button:has-text("로그인")').first().evaluate((el: any) => ({ + display: window.getComputedStyle(el).display, + backgroundColor: window.getComputedStyle(el).backgroundColor, + color: window.getComputedStyle(el).color, + padding: window.getComputedStyle(el).padding, + fontSize: window.getComputedStyle(el).fontSize, + cursor: window.getComputedStyle(el).cursor + })); + + console.log(` Display: ${buttonStyle.display}`); + console.log(` Background: ${buttonStyle.backgroundColor}`); + console.log(` Text Color: ${buttonStyle.color}`); + console.log(` Padding: ${buttonStyle.padding}`); + console.log(` Font Size: ${buttonStyle.fontSize}`); + + // 6️⃣ 전체 페이지 스크린샷 + console.log('\n6️⃣ 스크린샷 캡처...'); + + await page.screenshot({ + path: 'test-results/login-final-validation.png', + fullPage: true + }); + console.log(' ✅ test-results/login-final-validation.png'); + + // 7️⃣ 모바일 뷰 스크린샷 + await page.setViewportSize({ width: 375, height: 667 }); + await page.screenshot({ + path: 'test-results/login-mobile-view.png', + fullPage: true + }); + console.log(' ✅ test-results/login-mobile-view.png'); + + // 8️⃣ 최종 검증 + console.log('\n8️⃣ 스타일 검증 완료!'); + console.log(' ✅ 모든 스크린샷이 생성되었습니다'); + console.log(' ✅ test-results/ 폴더에서 확인하세요'); +});