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
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
- @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
+
@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
-
-
-
-
- Q
- QuantEngine
- 은퇴자산포트폴리오 투자 관리 시스템
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @if (!string.IsNullOrEmpty(_errorMessage))
- {
-
- @_errorMessage
-
- }
-
-
- @(_isSubmitting ? "인증 중..." : "로그인")
-
-
-
-
-
-
-
-@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"]
+
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(Model.ErrorMessage))
+ {
+
+ 오류: @Model.ErrorMessage
+
+ }
+
+ @if (!string.IsNullOrEmpty(Model.SuccessMessage))
+ {
+
+ @Model.SuccessMessage
+
+ }
+
+
+
+
+
+
+
+
+
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/ 폴더에서 확인하세요');
+});