chore: deprecate and remove blazor client code and project configurations
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 22s
Prepare Release / Build & Create Release (push) Failing after 58s
Prepare Release / Release Notification (push) Successful in 1s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled

This commit is contained in:
2026-07-12 13:06:57 +09:00
parent f8ff3a2c46
commit 6582ffc02a
36 changed files with 1254 additions and 3443 deletions
@@ -1,61 +0,0 @@
@namespace QuantEngine.Web.Client.Components
@inject IDialogService DialogService
@code {
public static async Task<bool> Show(IDialogService dialogService, string title, string message, string confirmText = "확인", string cancelText = "취소")
{
var options = new DialogOptions
{
CloseButton = false,
MaxWidth = MaxWidth.Small,
FullWidth = true,
BackdropClick = false
};
var parameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Title, title },
{ x => x.Message, message },
{ x => x.ConfirmText, confirmText },
{ x => x.CancelText, cancelText }
};
var dialog = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, options);
var result = await dialog.Result;
return !result.Canceled && (bool?)result.Data == true;
}
}
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudText Typo="Typo.h6">@Title</MudText>
<MudText Typo="Typo.body2">@Message</MudText>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Color="Color.Default">@CancelText</MudButton>
<MudButton OnClick="Confirm" Color="Color.Primary" Variant="Variant.Filled">@ConfirmText</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; }
[Parameter]
public string Title { get; set; } = "확인";
[Parameter]
public string Message { get; set; } = "";
[Parameter]
public string ConfirmText { get; set; } = "확인";
[Parameter]
public string CancelText { get; set; } = "취소";
private void Confirm() => MudDialog.Close(DialogResult.Ok(true));
private void Cancel() => MudDialog.Cancel();
}
@@ -1,125 +0,0 @@
@namespace QuantEngine.Web.Client.Components
<MudStack Spacing="2" Class="form-field">
<label class="form-label">
@Label
@if (Required)
{
<span class="text-error">*</span>
}
</label>
@switch (Type)
{
case "text":
case "email":
case "password":
case "number":
<MudTextField T="string"
Value="@Value"
ValueChanged="@((string v) => ValueChanged.InvokeAsync(v))"
Variant="Variant.Outlined"
FullWidth="true"
Placeholder="@Placeholder"
Type="@Type"
Required="@Required"
ErrorText="@ErrorMessage" />
break;
case "textarea":
<MudTextField T="string"
Value="@Value"
ValueChanged="@((string v) => ValueChanged.InvokeAsync(v))"
Variant="Variant.Outlined"
FullWidth="true"
Placeholder="@Placeholder"
Lines="5"
Required="@Required"
ErrorText="@ErrorMessage" />
break;
case "select":
<MudSelect T="string"
Value="@Value"
ValueChanged="@((string v) => ValueChanged.InvokeAsync(v))"
Variant="Variant.Outlined"
FullWidth="true"
Required="@Required">
@foreach (var option in Options)
{
<MudSelectItem T="string" Value="@option">@option</MudSelectItem>
}
</MudSelect>
break;
case "checkbox":
<MudCheckBox T="bool"
Checked="@(Value == "true")"
CheckedChanged="@((bool v) => ValueChanged.InvokeAsync(v ? "true" : "false"))">
@Label
</MudCheckBox>
break;
case "date":
<MudTextField T="string"
Value="@Value"
ValueChanged="@((string v) => ValueChanged.InvokeAsync(v))"
Variant="Variant.Outlined"
FullWidth="true"
Type="date"
Required="@Required" />
break;
}
@if (!string.IsNullOrEmpty(HelpText))
{
<MudText Typo="Typo.caption" Class="text-muted">@HelpText</MudText>
}
</MudStack>
@code {
[Parameter]
public string Label { get; set; } = "";
[Parameter]
public string Type { get; set; } = "text";
[Parameter]
public string Value { get; set; } = "";
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public string Placeholder { get; set; } = "";
[Parameter]
public bool Required { get; set; } = false;
[Parameter]
public string ErrorMessage { get; set; } = "";
[Parameter]
public string HelpText { get; set; } = "";
[Parameter]
public List<string> Options { get; set; } = new();
}
<style>
.form-field {
margin-bottom: 1rem;
}
.form-label {
display: block;
font-weight: 500;
font-size: 0.875rem;
color: var(--mud-palette-text-primary);
margin-bottom: 0.5rem;
}
.form-label .text-error {
color: var(--mud-palette-error);
}
</style>
@@ -1,249 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.JSInterop;
using QuantEngine.Web.Client.Services;
namespace QuantEngine.Web.Client.Infrastructure
{
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly LocalStorageService _localStorage;
private readonly HttpClient _http;
private readonly IJSRuntime _jsRuntime;
private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
private const string TokenKey = "quant_admin_access_token";
private const string UsernameKey = "quant_admin_username";
private const string RoleKey = "quant_admin_role";
private const string RememberUsernameKey = "quant_admin_remember_username";
private AuthenticationState? _cachedState;
public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http, IJSRuntime jsRuntime)
{
_localStorage = localStorage;
_http = http;
_jsRuntime = jsRuntime;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (_cachedState != null && _cachedState.User.Identity?.IsAuthenticated == true)
{
Console.WriteLine("[Auth] Returning cached authentication state");
return _cachedState;
}
try
{
Console.WriteLine("[Auth] GetAuthenticationStateAsync called");
// Primary: Try to validate via /api/auth/me
// This works with both cookies (automatic) and Bearer tokens
try
{
Console.WriteLine("[Auth] Attempting validation via /api/auth/me (cookie or Bearer)...");
// BaseAddress is always set to HostEnvironment.BaseAddress by DI.
// Never fall back to a hardcoded port — it breaks in production.
var meUrl = "api/auth/me";
var requestUri = _http.BaseAddress == null ? new Uri($"http://localhost:5265/{meUrl}") : new Uri(_http.BaseAddress, meUrl);
Console.WriteLine($"[Auth] /api/auth/me URL: {requestUri}");
var meResponse = await _http.GetAsync(requestUri);
Console.WriteLine($"[Auth] /api/auth/me status: {meResponse.StatusCode}");
if (meResponse.IsSuccessStatusCode)
{
var json = await meResponse.Content.ReadAsStringAsync();
Console.WriteLine($"[Auth] Response JSON: {json}");
var meData = System.Text.Json.JsonDocument.Parse(json).RootElement;
var authenticated = meData.TryGetProperty("authenticated", out var authProp) && authProp.GetBoolean();
var username = meData.TryGetProperty("username", out var userProp) ? userProp.GetString() : null;
var role = meData.TryGetProperty("role", out var roleProp) ? roleProp.GetString() : "Admin";
Console.WriteLine($"[Auth] Parsed: authenticated={authenticated}, username={username}, role={role}");
if (authenticated && !string.IsNullOrWhiteSpace(username))
{
Console.WriteLine($"[Auth] ✅ SUCCESS: Authenticated as {username}");
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role ?? "Admin")
}, "QuantAdminAuth");
var state = new AuthenticationState(new ClaimsPrincipal(identity));
_cachedState = state;
return state;
}
else
{
Console.WriteLine($"[Auth] Parsing failed: authenticated={authenticated}, username={username}");
}
}
else
{
Console.WriteLine($"[Auth] /api/auth/me returned {meResponse.StatusCode}");
if (IsLocalhost())
{
Console.WriteLine("[Auth] Dev SSR fallback: allowing admin authentication on 401");
return GetDevAdminState();
}
}
}
catch (Exception meEx)
{
Console.WriteLine($"[Auth] /api/auth/me failed: {meEx.Message}");
Console.WriteLine($"[Auth] Exception: {meEx}");
if (IsLocalhost())
{
Console.WriteLine("[Auth] Dev SSR fallback: allowing admin authentication on exception");
return GetDevAdminState();
}
}
// Fallback: Try to read from localStorage
Console.WriteLine("[Auth] Fallback: checking localStorage...");
try
{
string token = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", TokenKey);
string username = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", UsernameKey);
string role = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", RoleKey);
Console.WriteLine($"[Auth] localStorage: token={!string.IsNullOrWhiteSpace(token)}, username={username}");
if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username))
{
var meUrl = "api/auth/me";
var requestUri = _http.BaseAddress == null ? new Uri($"http://localhost:5265/{meUrl}") : new Uri(_http.BaseAddress, meUrl);
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await _http.SendAsync(request);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"[Auth] ✅ localStorage token validated: {username}");
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role ?? "Admin")
}, "QuantAdminAuth");
var state = new AuthenticationState(new ClaimsPrincipal(identity));
_cachedState = state;
return state;
}
}
}
catch (Exception jsEx)
{
Console.WriteLine($"[Auth] localStorage fallback failed: {jsEx.Message}");
}
Console.WriteLine("[Auth] ❌ Not authenticated");
}
catch (Exception ex)
{
Console.WriteLine($"[Auth] Unexpected error: {ex.Message}");
}
_cachedState = new AuthenticationState(_anonymous);
return _cachedState;
}
public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role)
{
await MarkUserAsAuthenticatedAsync(username, accessToken, role, rememberUsername: true);
}
public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role, bool rememberUsername)
{
await _localStorage.SetAsync(TokenKey, accessToken);
if (rememberUsername)
{
await _localStorage.SetAsync(UsernameKey, username);
await _localStorage.SetAsync(RememberUsernameKey, true);
}
else
{
await _localStorage.DeleteAsync(UsernameKey);
await _localStorage.SetAsync(RememberUsernameKey, false);
}
await _localStorage.SetAsync(RoleKey, role);
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role)
}, "QuantAdminAuth");
var user = new ClaimsPrincipal(identity);
var state = new AuthenticationState(user);
_cachedState = state;
NotifyAuthenticationStateChanged(Task.FromResult(state));
}
public async Task MarkUserAsLoggedOutAsync()
{
await _localStorage.DeleteAsync(TokenKey);
await _localStorage.DeleteAsync(RoleKey);
var rememberUsername = await _localStorage.GetAsync<bool>(RememberUsernameKey);
if (!rememberUsername)
{
await _localStorage.DeleteAsync(UsernameKey);
}
_cachedState = new AuthenticationState(_anonymous);
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
}
public async Task LogoutFromServerAsync()
{
var token = await _localStorage.GetAsync<string>(TokenKey);
if (!string.IsNullOrWhiteSpace(token))
{
try
{
var request = new HttpRequestMessage(HttpMethod.Post, "api/auth/logout");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
await _http.SendAsync(request);
}
catch
{
// Best-effort server revocation; always clear local state.
}
}
await MarkUserAsLoggedOutAsync();
}
public async Task<string?> GetRememberedUsernameAsync()
{
var rememberUsername = await _localStorage.GetAsync<bool>(RememberUsernameKey);
if (!rememberUsername)
{
return null;
}
return await _localStorage.GetAsync<string>(UsernameKey);
}
private bool IsLocalhost()
{
return _http.BaseAddress == null || _http.BaseAddress.Host == "localhost" || _http.BaseAddress.Host == "127.0.0.1";
}
private AuthenticationState GetDevAdminState()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "admin"),
new Claim(ClaimTypes.Role, "Admin")
}, "QuantAdminAuth");
var state = new AuthenticationState(new ClaimsPrincipal(identity));
_cachedState = state;
return state;
}
}
}
@@ -1,20 +0,0 @@
@inherits LayoutComponentBase
@rendermode InteractiveWebAssembly
<style>
:global(body) {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
:global(html, body, #app) {
width: 100%;
height: 100%;
}
</style>
@Body
@code {
}
@@ -1,260 +0,0 @@
/* QuantEngine AuthLayout Styles */
.auth-container {
display: flex;
min-height: 100vh;
background: linear-gradient(135deg, var(--mud-palette-primary) 0%, var(--mud-palette-primary-dark) 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Left Panel - Branding */
.auth-left-panel {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 3rem;
color: white;
position: relative;
}
.auth-branding {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex: 1;
justify-content: center;
}
.auth-logo {
margin-bottom: 2rem;
animation: float 3s ease-in-out infinite;
}
.auth-logo ::deep svg {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
font-size: 80px;
color: white;
}
.auth-title {
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: 1px;
}
.auth-subtitle {
opacity: 0.9;
font-size: 1.1rem;
max-width: 300px;
}
.auth-features {
margin-top: 3rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: flex-start;
width: 100%;
max-width: 300px;
}
.auth-feature {
display: flex;
align-items: center;
gap: 1rem;
opacity: 0.95;
}
.auth-feature ::deep svg {
font-size: 24px;
color: #4caf50;
flex-shrink: 0;
}
.auth-theme-toggle {
position: absolute;
top: 2rem;
right: 2rem;
}
.auth-theme-toggle ::deep button {
color: white;
transition: transform 0.2s ease;
}
.auth-theme-toggle ::deep button:hover {
transform: scale(1.1);
}
/* Right Panel - Auth Content */
.auth-right-panel {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
background: var(--mud-palette-background);
position: relative;
}
.auth-mobile-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--mud-palette-divider);
}
.auth-mobile-header ::deep .mud-icon {
color: var(--mud-palette-primary);
}
.auth-content {
width: 100%;
max-width: 450px;
}
.auth-content ::deep .mud-card {
background: var(--mud-palette-surface);
border: 1px solid var(--mud-palette-divider);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.auth-content ::deep .mud-form-control {
margin-bottom: 1.5rem;
}
.auth-content ::deep .mud-button {
text-transform: none;
font-weight: 600;
padding: 0.75rem 1.5rem;
}
.auth-content ::deep .mud-button-root {
border-radius: 0.4rem;
}
/* Footer */
.auth-footer {
position: absolute;
bottom: 2rem;
text-align: center;
width: 100%;
padding: 1rem 2rem;
border-top: 1px solid var(--mud-palette-divider);
}
.auth-footer-text {
display: block;
color: var(--mud-palette-text-secondary);
margin-bottom: 0.5rem;
}
.auth-footer-links {
display: flex;
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.auth-footer-links ::deep a {
color: var(--mud-palette-primary);
text-decoration: none;
transition: color 0.2s ease;
}
.auth-footer-links ::deep a:hover {
color: var(--mud-palette-primary-dark);
text-decoration: underline;
}
/* Responsive */
@media (max-width: 960px) {
.auth-container {
flex-direction: column;
}
.auth-left-panel {
padding: 2rem;
min-height: 40vh;
}
.auth-right-panel {
padding: 3rem 2rem 5rem;
min-height: 60vh;
}
.auth-mobile-header {
display: flex;
}
.auth-footer {
bottom: 1rem;
padding: 1rem;
}
}
@media (max-width: 600px) {
.auth-right-panel {
padding: 2rem 1rem 5rem;
}
.auth-content {
max-width: 100%;
}
.auth-features {
max-width: 100%;
}
.auth-footer {
position: static;
padding: 1rem;
border-top: 1px solid var(--mud-palette-divider);
margin-top: 3rem;
}
}
/* Animation */
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* Dark Mode */
[data-theme="dark"] .auth-container {
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%);
}
[data-theme="dark"] .auth-left-panel {
color: #f0f0f0;
}
[data-theme="dark"] .auth-right-panel {
background: #121212;
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
.auth-logo {
animation: none;
}
.auth-theme-toggle ::deep button {
transition: none;
}
.auth-footer-links ::deep a {
transition: none;
}
}
@@ -1,16 +0,0 @@
@inherits LayoutComponentBase
@Body
<style>
:global(html, body) {
height: 100%;
margin: 0;
padding: 0;
}
:global(#app) {
display: flex;
min-height: 100vh;
}
</style>
@@ -1,154 +0,0 @@
@inherits LayoutComponentBase
@using QuantEngine.Web.Client.Theme
@inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
<!-- ✅ MudBlazor Providers (Required for Interactive WebAssembly) -->
<MudThemeProvider Theme="@_theme" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<!-- Top Navigation Bar -->
<MudAppBar Elevation="1" Dense="false" Color="Color.Surface" Class="mud-appbar-dense">
<MudHidden Breakpoint="Breakpoint.SmAndUp" Invert="true">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@(() => navOpen = !navOpen)" />
</MudHidden>
<MudText Typo="Typo.h6" Class="ml-2">
<MudIcon Icon="@Icons.Material.Filled.Dashboard" Class="me-2" />
QuantEngine
</MudText>
<MudSpacer />
<!-- User Menu -->
<AuthorizeView Context="authContext">
<Authorized>
<MudMenu AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopRight" Class="ml-2">
<ActivatorContent>
<MudAvatar Color="Color.Primary" Image="@GetUserInitials()" Class="cursor-pointer">
@GetFirstLetter(authContext.User.Identity?.Name)
</MudAvatar>
</ActivatorContent>
<ChildContent>
<MudMenuItem>
<MudText Typo="Typo.body2">
<strong>@authContext.User.Identity?.Name</strong>
</MudText>
</MudMenuItem>
<MudDivider />
<MudMenuItem href="/profile">
<MudIcon Icon="@Icons.Material.Filled.Person" Class="mr-2" Size="Size.Small" />
프로필
</MudMenuItem>
<MudMenuItem href="/settings">
<MudIcon Icon="@Icons.Material.Filled.Settings" Class="mr-2" Size="Size.Small" />
설정
</MudMenuItem>
<MudDivider />
<MudMenuItem OnClick="HandleLogoutAsync">
<MudIcon Icon="@Icons.Material.Filled.Logout" Class="mr-2" Size="Size.Small" Color="Color.Error" />
<MudText Color="Color.Error">로그아웃</MudText>
</MudMenuItem>
</ChildContent>
</MudMenu>
</Authorized>
</AuthorizeView>
</MudAppBar>
<!-- Sidebar Navigation -->
<MudDrawer Open="@navOpen" Variant="DrawerVariant.Responsive" Elevation="1" FixedOpen="@fixedOpen">
<MudDrawerHeader Class="d-flex align-center justify-space-between">
<MudText Typo="Typo.h6" Class="px-2">메뉴</MudText>
<MudHidden Breakpoint="Breakpoint.Md" Invert="true">
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
OnClick="ToggleDrawer"
Class="mx-1" />
</MudHidden>
</MudDrawerHeader>
<MudNavMenu>
<NavMenu />
</MudNavMenu>
<!-- Drawer Footer -->
<div class="mud-drawer-footer">
<MudDivider />
<div style="padding: 16px;">
<MudText Typo="Typo.caption">
<strong>QuantEngine</strong>
</MudText>
<MudText Typo="Typo.caption">
v@appVersion
</MudText>
<MudText Typo="Typo.caption" Class="mt-2">
배포: @buildTime
</MudText>
</div>
</div>
</MudDrawer>
<!-- Main Content Area -->
<MudMainContent Class="mud-main-content-enhanced">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-6">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
@code {
private MudTheme _theme = AppTheme.LightTheme;
private bool navOpen = true;
private bool fixedOpen = true;
private string appVersion = "Local Debug";
private string buildTime = "N/A";
protected override async Task OnInitializedAsync()
{
try
{
var versionInfo = await Http.GetFromJsonAsync<VersionInfo>("version.json");
if (versionInfo != null)
{
appVersion = versionInfo.Version ?? "Local Debug";
buildTime = versionInfo.Built ?? "N/A";
}
}
catch
{
}
await base.OnInitializedAsync();
}
private void ToggleDrawer()
{
navOpen = !navOpen;
}
private async Task HandleLogoutAsync()
{
var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
await customProvider.LogoutFromServerAsync();
NavigationManager.NavigateTo("/Account/Login", forceLoad: true);
}
private string GetFirstLetter(string? name)
{
return string.IsNullOrEmpty(name) ? "?" : name[0].ToString().ToUpper();
}
private string GetUserInitials()
{
return string.Empty;
}
private class VersionInfo
{
public string? Version { get; set; }
public string? Built { get; set; }
}
}
@@ -1,105 +0,0 @@
/* QuantEngine MainLayout Styles */
/* AppBar Enhancements */
.mud-appbar-dense {
padding: 0 1rem;
}
.mud-appbar-dense ::deep .mud-appbar-section-center {
flex: 1;
}
/* Avatar Styling */
::deep .mud-avatar {
cursor: pointer;
transition: transform 0.2s ease;
}
::deep .mud-avatar:hover {
transform: scale(1.05);
}
/* Drawer Footer */
.mud-drawer-footer {
position: absolute;
bottom: 0;
width: 100%;
background: var(--mud-palette-surface);
}
/* Main Content Area */
.mud-main-content-enhanced {
min-height: 100vh;
background: var(--mud-palette-background);
transition: background-color 0.3s ease;
}
/* Navigation Menu Styles */
.mud-navmenu {
padding: 1rem 0;
}
.mud-navmenu ::deep .mud-nav-item {
padding: 0.5rem 0;
margin: 0.25rem 0;
}
.mud-navmenu ::deep .mud-nav-link {
border-radius: 0.4rem;
margin: 0 0.5rem;
transition: all 0.2s ease;
}
.mud-navmenu ::deep .mud-nav-link:hover {
background-color: var(--mud-palette-action-default-hover);
}
.mud-navmenu ::deep .mud-nav-link.mud-ripple-nav-link-active {
background-color: var(--mud-palette-primary-lighten);
color: var(--mud-palette-primary);
font-weight: 600;
}
/* Responsive Drawer */
@media (max-width: 599px) {
.mud-drawer-content {
width: 100% !important;
}
.mud-drawer-footer {
position: relative;
}
}
@media (min-width: 600px) {
.mud-drawer-footer {
position: absolute;
}
}
/* Error UI */
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
/* Dark Mode Transitions */
* {
transition: background-color 0.3s ease, color 0.3s ease;
}
@@ -1,18 +0,0 @@
<MudNavMenu>
<!-- Main Navigation -->
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard" Match="NavLinkMatch.All">
대시보드
</MudNavLink>
<!-- Admin Section -->
<MudNavGroup Title="관리" Icon="@Icons.Material.Filled.AdminPanelSettings" Expanded="true">
<MudNavLink Href="/users" Icon="@Icons.Material.Filled.People">사용자 관리</MudNavLink>
<MudNavLink Href="/collection" Icon="@Icons.Material.Filled.CloudDownload">데이터 수집</MudNavLink>
<MudNavLink Href="/monitoring" Icon="@Icons.Material.Filled.Timeline">수집 모니터링</MudNavLink>
</MudNavGroup>
<!-- Operations -->
<MudNavLink Href="/operations" Icon="@Icons.Material.Filled.PlaylistPlay" Match="NavLinkMatch.Prefix">
운영 리포트
</MudNavLink>
</MudNavMenu>
@@ -1,105 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
@@ -1,155 +0,0 @@
@page "/collection"
@attribute [Authorize]
@using QuantEngine.Web.Client.Services
@inject ApiClient ApiClient
@inject ILogger<Collection> Logger
<PageTitle>QuantEngine - Collection</PageTitle>
<MudText Typo="Typo.h4" Class="mb-2">Data Collection</MudText>
<MudText Typo="Typo.body2" Class="mb-4">KIS API data collection dashboard. API-first로만 동작합니다.</MudText>
<MudStack Row="true" Spacing="2" Class="mb-4">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@StartCollectionAsync" Disabled="@IsProcessing">
@(IsProcessing ? "Running..." : "Start Collection")
</MudButton>
<MudButton Variant="Variant.Outlined" OnClick="@RefreshAsync" Disabled="@IsProcessing">Refresh</MudButton>
</MudStack>
@if (IsLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-4" />
}
else if (DashboardState != null)
{
<MudGrid Spacing="2" Class="mb-4">
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Last Run</MudText>
<MudText Typo="Typo.h6">@(DashboardState.LastRunStatus ?? "N/A")</MudText>
<MudText Typo="Typo.body2">@(DashboardState.LastFinishedAt ?? "Not finished")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Total Snapshots</MudText>
<MudText Typo="Typo.h6">@DashboardState.TotalSnapshots</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Total Errors</MudText>
<MudText Typo="Typo.h6">@DashboardState.TotalErrors</MudText>
</MudPaper>
</MudItem>
</MudGrid>
@if (DashboardState.RecentErrors.Count > 0)
{
<MudPaper Class="pa-4 mb-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">Recent Errors</MudText>
<MudTable Items="@DashboardState.RecentErrors" Dense="true" Hover="true">
<HeaderContent>
<MudTh>Source</MudTh>
<MudTh>Kind</MudTh>
<MudTh>Ticker</MudTh>
<MudTh>Message</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Source">@context.SourceName</MudTd>
<MudTd DataLabel="Kind">@context.ErrorKind</MudTd>
<MudTd DataLabel="Ticker">@context.Ticker</MudTd>
<MudTd DataLabel="Message">@context.ErrorMessage</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
}
@if (RecentRuns != null && RecentRuns.Count > 0)
{
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">Recent Runs</MudText>
<MudTable Items="@RecentRuns" Dense="true" Hover="true">
<HeaderContent>
<MudTh>Run ID</MudTh>
<MudTh>Status</MudTh>
<MudTh>Started</MudTh>
<MudTh>Finished</MudTh>
<MudTh>Snapshots</MudTh>
<MudTh>Errors</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Run ID" Style="font-family: monospace; font-size: 12px;">@context.RunId</MudTd>
<MudTd DataLabel="Status">@context.Status</MudTd>
<MudTd DataLabel="Started">@context.StartedAt</MudTd>
<MudTd DataLabel="Finished">@context.FinishedAt</MudTd>
<MudTd DataLabel="Snapshots">@context.TotalSnapshots</MudTd>
<MudTd DataLabel="Errors">@context.TotalErrors</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
}
}
@code {
private CollectionDashboardStateDto? DashboardState;
private List<CollectionRunDto>? RecentRuns;
private bool IsLoading = true;
private bool IsProcessing = false;
protected override async Task OnInitializedAsync()
{
await LoadDashboardStateAsync();
}
private async Task LoadDashboardStateAsync()
{
IsLoading = true;
try
{
// Parallelize API calls to avoid sequential RTT bottlenecks
var stateTask = ApiClient.GetCollectionStateAsync();
var runsTask = ApiClient.GetCollectionRunsAsync(10);
await Task.WhenAll(stateTask, runsTask);
DashboardState = await stateTask;
var runsResponse = await runsTask;
RecentRuns = runsResponse?.Runs ?? new();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading dashboard");
}
finally
{
IsLoading = false;
}
}
private async Task StartCollectionAsync()
{
IsProcessing = true;
try
{
var result = await ApiClient.StartCollectionRunAsync();
if (result != null)
{
await LoadDashboardStateAsync();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error starting collection");
}
finally
{
IsProcessing = false;
}
}
private async Task RefreshAsync()
{
await LoadDashboardStateAsync();
}
}
@@ -1,342 +0,0 @@
@page "/dashboard"
@rendermode InteractiveWebAssembly
@using QuantEngine.Core.Infrastructure
@using Microsoft.AspNetCore.Components.Authorization
@inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavManager
<PageTitle>QuantEngine - Admin Dashboard</PageTitle>
<!-- Page Header -->
<div class="mb-6">
<MudText Typo="Typo.h4" Class="mb-2">관리자 대시보드</MudText>
<MudText Typo="Typo.body1" Class="text-muted">시스템 현황 및 데이터 수집 모니터링</MudText>
</div>
<!-- KPI Cards -->
<MudGrid Spacing="3" Class="mb-6">
<!-- Total Runs -->
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4 mud-card-kpi" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<div class="d-flex justify-content-between align-items-start">
<div>
<MudText Typo="Typo.caption" Class="text-muted mb-1">총 수집 실행</MudText>
<MudText Typo="Typo.h5" Class="text-primary">@TotalRuns</MudText>
<MudText Typo="Typo.body2" Class="text-muted mt-2">
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Size="Size.Small" Style="color: #4caf50;" />
이번 주 +@WeeklyRuns
</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.PlayCircleOutline" Size="Size.Large" Class="text-primary" Style="opacity: 0.3;" />
</div>
</MudPaper>
</MudItem>
<!-- Success Rate -->
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4 mud-card-kpi" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<div class="d-flex justify-content-between align-items-start">
<div>
<MudText Typo="Typo.caption" Class="text-muted mb-1">성공률</MudText>
<MudText Typo="Typo.h5" Class="text-success">@SuccessRate%</MudText>
<MudText Typo="Typo.body2" Class="text-muted mt-2">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Style="color: #4caf50;" />
최근 30일
</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.Assessment" Size="Size.Large" Class="text-success" Style="opacity: 0.3;" />
</div>
</MudPaper>
</MudItem>
<!-- Recent Errors -->
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4 mud-card-kpi" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<div class="d-flex justify-content-between align-items-start">
<div>
<MudText Typo="Typo.caption" Class="text-muted mb-1">최근 에러</MudText>
<MudText Typo="Typo.h5" Class="text-error">@RecentErrors</MudText>
<MudText Typo="Typo.body2" Class="text-muted mt-2">
<MudIcon Icon="@Icons.Material.Filled.ErrorOutline" Size="Size.Small" Style="color: #f44336;" />
지난 7일
</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.WarningAmber" Size="Size.Large" Class="text-error" Style="opacity: 0.3;" />
</div>
</MudPaper>
</MudItem>
<!-- Last Sync -->
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4 mud-card-kpi" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<div class="d-flex justify-content-between align-items-start">
<div>
<MudText Typo="Typo.caption" Class="text-muted mb-1">마지막 동기화</MudText>
<MudText Typo="Typo.h5">@LastSyncTime</MudText>
<MudText Typo="Typo.body2" Class="text-muted mt-2">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@(IsLastSyncSuccess ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(IsLastSyncSuccess ? "성공" : "경고")
</MudChip>
</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Large" Class="text-secondary" Style="opacity: 0.3;" />
</div>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Main Content Grid -->
<MudGrid Spacing="3" Class="mb-6">
<!-- Recent Activity Feed -->
<MudItem xs="12" md="8">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">최근 활동</MudText>
@if (RecentActivities.Count == 0)
{
<MudAlert Severity="Severity.Info">활동 기록이 없습니다.</MudAlert>
}
else
{
<MudStack Spacing="2">
@foreach (var activity in RecentActivities)
{
<div class="d-flex gap-3 pa-2" style="border-left: 3px solid @GetActivityColor(activity.Type); padding-left: 12px;">
<MudIcon Icon="@GetActivityIcon(activity.Type)" Size="Size.Medium" Color="@GetActivityColorEnum(activity.Type)" />
<div style="flex: 1;">
<MudText Typo="Typo.body2" Class="font-weight-500">@activity.Title</MudText>
<MudText Typo="Typo.caption" Class="text-muted">@activity.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")</MudText>
<MudText Typo="Typo.body2" Class="mt-1">@activity.Description</MudText>
</div>
</div>
}
</MudStack>
}
</MudPaper>
</MudItem>
<!-- System Status -->
<MudItem xs="12" md="4">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">시스템 상태</MudText>
<MudStack Spacing="2">
<div class="d-flex justify-content-between align-items-center">
<MudText Typo="Typo.body2">API 서버</MudText>
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">온라인</MudChip>
</div>
<div class="d-flex justify-content-between align-items-center">
<MudText Typo="Typo.body2">데이터베이스</MudText>
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">연결됨</MudChip>
</div>
<div class="d-flex justify-content-between align-items-center">
<MudText Typo="Typo.body2">KIS API</MudText>
<MudChip T="string" Label="true" Size="Size.Small" Color="@(KisApiStatus ? Color.Success : Color.Warning)" Variant="Variant.Filled">
@(KisApiStatus ? "활성" : "비활성")
</MudChip>
</div>
<MudDivider Class="my-2" />
<MudText Typo="Typo.caption" Class="text-muted">마지막 점검: @SystemCheckTime</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Collections Table -->
<MudPaper Class="pa-4" Elevation="1">
<div class="d-flex justify-content-between align-items-center mb-4">
<MudText Typo="Typo.h6">최근 데이터 수집 실행</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" OnClick="RefreshData">
<MudIcon Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" Class="mr-2" />
새로고침
</MudButton>
</div>
@if (Sections.Count == 0)
{
<MudAlert Severity="Severity.Info">데이터 수집 기록이 없습니다.</MudAlert>
}
else
{
<MudTable Items="@Sections" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>이름</MudTh>
<MudTh>상태</MudTh>
<MudTh>시작 시간</MudTh>
<MudTh>작업</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">
<MudText Typo="Typo.body2">@context.Name</MudText>
</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
@context.Title
</MudChip>
</MudTd>
<MudTd DataLabel="Timestamp">
<MudText Typo="Typo.body2">@context.Preview</MudText>
</MudTd>
<MudTd DataLabel="Actions">
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary">상세</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
<style>
.mud-card-kpi {
border-radius: 8px !important;
transition: all 0.3s ease;
}
.mud-card-kpi:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
transform: translateY(-2px);
}
.text-primary {
color: var(--mud-palette-primary) !important;
}
.text-success {
color: var(--mud-palette-success) !important;
}
.text-error {
color: var(--mud-palette-error) !important;
}
.text-muted {
color: var(--mud-palette-text-secondary) !important;
}
.font-weight-500 {
font-weight: 500;
}
.gap-3 {
gap: 1rem;
}
</style>
@code {
private readonly List<OperationalReportSection> Sections = new();
private readonly List<ActivityLog> RecentActivities = new();
// KPI values
private int TotalRuns = 47;
private int WeeklyRuns = 12;
private int SuccessRate = 94;
private int RecentErrors = 3;
private string LastSyncTime = "2분 전";
private bool IsLastSyncSuccess = true;
private bool KisApiStatus = true;
private string SystemCheckTime = DateTime.Now.ToString("HH:mm:ss");
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
if (!(authState.User.Identity?.IsAuthenticated ?? false))
{
NavManager.NavigateTo("/Account/Login", forceLoad: true);
return;
}
try
{
var report = await Http.GetFromJsonAsync<OperationalReportData>("api/operational-report");
if (report != null)
{
Sections.Clear();
Sections.AddRange(report.Sections);
}
}
catch
{
// Handle error silently
}
LoadRecentActivities();
}
private void LoadRecentActivities()
{
RecentActivities.Clear();
RecentActivities.AddRange(new[]
{
new ActivityLog
{
Type = "success",
Title = "데이터 수집 완료",
Description = "삼성전자(005930) 주가 데이터 수집 성공",
Timestamp = DateTime.Now.AddMinutes(-5)
},
new ActivityLog
{
Type = "warning",
Title = "API 레이트 제한",
Description = "KIS API 레이트 제한에 도달했으나 재시도 예정",
Timestamp = DateTime.Now.AddMinutes(-12)
},
new ActivityLog
{
Type = "success",
Title = "대시보드 업데이트",
Description = "포트폴리오 구성 분석 완료",
Timestamp = DateTime.Now.AddMinutes(-35)
},
new ActivityLog
{
Type = "info",
Title = "스케줄 실행",
Description = "일일 정기 수집 작업 시작",
Timestamp = DateTime.Now.AddHours(-1)
}
});
}
private async Task RefreshData()
{
await OnInitializedAsync();
}
private string GetActivityIcon(string type) => type switch
{
"success" => Icons.Material.Filled.CheckCircle,
"warning" => Icons.Material.Filled.WarningAmber,
"error" => Icons.Material.Filled.Error,
_ => Icons.Material.Filled.Info
};
private string GetActivityColor(string type) => type switch
{
"success" => "#4caf50",
"warning" => "#ff9800",
"error" => "#f44336",
_ => "#2196f3"
};
private Color GetActivityColorEnum(string type) => type switch
{
"success" => Color.Success,
"warning" => Color.Warning,
"error" => Color.Error,
_ => Color.Info
};
private class ActivityLog
{
public string Type { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime Timestamp { get; set; }
}
}
@@ -1,229 +0,0 @@
@page "/monitoring"
@attribute [Authorize]
@inject HttpClient Http
@inject ISnackbar Snackbar
<PageTitle>QuantEngine - 데이터 수집 모니터링</PageTitle>
<!-- Page Header -->
<div class="mb-6">
<div class="d-flex justify-content-between align-items-center">
<div>
<MudText Typo="Typo.h4" Class="mb-2">데이터 수집 모니터링</MudText>
<MudText Typo="Typo.body1" Class="text-muted">실시간 수집 작업 상태 및 에러 추적</MudText>
</div>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Size="Size.Small"
OnClick="RefreshAsync" Disabled="_loading">
<MudIcon Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" Class="mr-2" />
새로고침
</MudButton>
</div>
</div>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-4" />
}
<!-- Collection Status Cards -->
<MudGrid Spacing="3" Class="mb-6">
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">진행 중인 작업</MudText>
<MudText Typo="Typo.h5">@_runningCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">완료</MudText>
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-success);">@_completedCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">실패</MudText>
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-error);">@_failedCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">총 스냅샷</MudText>
<MudText Typo="Typo.h5">@_totalSnapshots</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Tabs -->
<MudTabs Outlined="true" Class="mb-6">
<!-- Recent Runs -->
<MudTabPanel Text="최근 실행">
<div class="py-4">
<MudPaper Class="pa-4" Elevation="1">
@if (_recentRuns.Count == 0 && !_loading)
{
<MudAlert Severity="Severity.Info">최근 실행 기록이 없습니다.</MudAlert>
}
else
{
<MudTable Items="@_recentRuns" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>실행 ID</MudTh>
<MudTh>시작 시간</MudTh>
<MudTh>종료 시간</MudTh>
<MudTh>상태</MudTh>
<MudTh>스냅샷</MudTh>
<MudTh>에러</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Run ID">
<MudText Typo="Typo.body2" Class="font-monospace">@context.RunId</MudText>
</MudTd>
<MudTd DataLabel="Start">
<MudText Typo="Typo.body2">@FormatTime(context.StartedAt)</MudText>
</MudTd>
<MudTd DataLabel="End">
<MudText Typo="Typo.body2">@(string.IsNullOrEmpty(context.FinishedAt) ? "-" : FormatTime(context.FinishedAt))</MudText>
</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@GetStatusColor(context.Status)"
Variant="Variant.Filled">
@context.Status
</MudChip>
</MudTd>
<MudTd DataLabel="Snapshots">
<MudText Typo="Typo.body2">@(context.TotalSnapshots?.ToString() ?? "-")</MudText>
</MudTd>
<MudTd DataLabel="Errors">
@if (context.TotalErrors > 0)
{
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">
@context.TotalErrors
</MudChip>
}
else
{
<MudText Typo="Typo.body2">-</MudText>
}
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
</div>
</MudTabPanel>
<!-- Error Logs -->
<MudTabPanel Text="에러 로그">
<div class="py-4">
<MudPaper Class="pa-4" Elevation="1">
@if (_errors.Count == 0 && !_loading)
{
<MudAlert Severity="Severity.Success">에러가 없습니다.</MudAlert>
}
else
{
<MudStack Spacing="2">
@foreach (var error in _errors)
{
<div class="pa-3" style="border-left: 3px solid #f44336; background-color: var(--mud-palette-surface);">
<div class="d-flex justify-content-between align-items-start mb-2">
<MudText Typo="Typo.body2" Class="font-weight-500">[@error.ErrorKind] @error.ErrorMessage</MudText>
<MudText Typo="Typo.caption" Class="text-muted">@FormatTime(error.CreatedAt)</MudText>
</div>
<MudText Typo="Typo.caption" Class="text-muted">Run: @error.RunId</MudText>
@if (!string.IsNullOrEmpty(error.Ticker))
{
<MudText Typo="Typo.caption" Class="text-muted ml-3">Ticker: @error.Ticker</MudText>
}
</div>
}
</MudStack>
}
</MudPaper>
</div>
</MudTabPanel>
</MudTabs>
@code {
private bool _loading = false;
private int _runningCount;
private int _completedCount;
private int _failedCount;
private int _totalSnapshots;
private List<CollectionRunDto> _recentRuns = new();
private List<CollectionErrorDto> _errors = new();
protected override async Task OnInitializedAsync()
{
await RefreshAsync();
}
private async Task RefreshAsync()
{
_loading = true;
StateHasChanged();
try
{
// 최근 실행 목록 로드
var runsResponse = await Http.GetFromJsonAsync<CollectionRunsResponse>("api/collection/runs?limit=20");
if (runsResponse?.Runs is not null)
{
_recentRuns = runsResponse.Runs;
_runningCount = _recentRuns.Count(r => string.Equals(r.Status, "running", StringComparison.OrdinalIgnoreCase));
_completedCount = _recentRuns.Count(r => string.Equals(r.Status, "completed", StringComparison.OrdinalIgnoreCase)
|| string.Equals(r.Status, "PASS", StringComparison.OrdinalIgnoreCase));
_failedCount = _recentRuns.Count(r => string.Equals(r.Status, "failed", StringComparison.OrdinalIgnoreCase)
|| string.Equals(r.Status, "error", StringComparison.OrdinalIgnoreCase));
_totalSnapshots = _recentRuns.Sum(r => r.TotalSnapshots ?? 0);
}
// 대시보드 상태 로드 (전체 오류 목록)
var state = await Http.GetFromJsonAsync<CollectionDashboardStateDto>("api/collection/state");
if (state?.RecentErrors is not null)
{
_errors = state.RecentErrors;
}
}
catch (Exception ex)
{
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private Color GetStatusColor(string status) => status?.ToLowerInvariant() switch
{
"running" => Color.Info,
"completed" => Color.Success,
"pass" => Color.Success,
"failed" => Color.Error,
"error" => Color.Error,
_ => Color.Warning
};
private string FormatTime(string? isoTime)
{
if (string.IsNullOrEmpty(isoTime)) return "-";
return DateTimeOffset.TryParse(isoTime, out var dt)
? dt.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss")
: isoTime;
}
// DTOs (shared with ApiClient)
private record CollectionRunsResponse(List<CollectionRunDto> Runs, int Count);
private record CollectionRunDto(
string RunId, string Status, string StartedAt,
string? FinishedAt, int? TotalSnapshots, int? TotalErrors);
private record CollectionDashboardStateDto(
string? LastRunId, string? LastRunStatus, string? LastFinishedAt,
int TotalSnapshots, int TotalErrors, List<CollectionErrorDto> RecentErrors);
private record CollectionErrorDto(
string RunId, string SourceName, string ErrorKind,
string ErrorMessage, string? Ticker, string CreatedAt);
}
@@ -1,8 +0,0 @@
@page "/not-found"
@layout MainLayout
<!-- 🎯 DEBUG MARKER: NOTFOUND_RENDERING -->
<div id="notfound-debug-marker" style="display:none;">NOTFOUND_RENDERING_ACTIVE</div>
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
@@ -1,121 +0,0 @@
@page "/operations"
@attribute [Authorize]
@using QuantEngine.Core.Infrastructure
@inject HttpClient Http
<PageTitle>QuantEngine - Operations</PageTitle>
<MudText Typo="Typo.h4" Class="mb-2">Operational Report</MudText>
<MudText Typo="Typo.body2" Class="mb-4">Temp/operational_report.json만 읽는 운영 고정 화면입니다.</MudText>
<MudGrid Spacing="2" Class="mb-4">
<MudItem xs="12" sm="3">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Schema</MudText>
<MudText Typo="Typo.h6">@SchemaVersion</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="3">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Sections</MudText>
<MudText Typo="Typo.h6">@SectionCountLabel</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="3">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Source</MudText>
<MudText Typo="Typo.h6">@SourceJson</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="3">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">Generated</MudText>
<MudText Typo="Typo.h6">@GeneratedAt</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudGrid Spacing="2" Class="mb-4">
@foreach (var section in HighlightSections)
{
<MudItem xs="12" sm="6" md="3" @key="section.Name">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.caption">@(section.Name)</MudText>
<MudText Typo="Typo.h6">@(section.Title)</MudText>
<MudText Typo="Typo.body2">@(section.Preview)</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
<MudPaper Class="pa-4 mb-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">Report Health</MudText>
<MudStack Spacing="1">
<MudText Typo="Typo.body2">Status: <MudChip T="string" Color="@(HealthLabel == "PASS" ? Color.Success : Color.Warning)" Variant="Variant.Filled">@HealthLabel</MudChip></MudText>
<MudText Typo="Typo.body2">Path: @ReportPath</MudText>
<MudText Typo="Typo.body2">Sections rendered: @RenderedSectionCountLabel</MudText>
</MudStack>
</MudPaper>
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">Sections</MudText>
@if (Sections.Count == 0)
{
<MudAlert Severity="Severity.Warning">DATA_MISSING: operational_report.json에 표시할 섹션이 없습니다.</MudAlert>
}
else
{
<MudTable Items="@Sections" Dense="true" Hover="true">
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Title</MudTh>
<MudTh>Preview</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Title">@context.Title</MudTd>
<MudTd DataLabel="Preview">@context.Preview</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
@code {
private readonly List<OperationalReportSection> Sections = new();
private readonly List<OperationalReportSection> HighlightSections = new();
private string SchemaVersion = "n/a";
private string SourceJson = "n/a";
private string GeneratedAt = "n/a";
private string SectionCountLabel = "0";
private string RenderedSectionCountLabel = "0";
private string HealthLabel = "DATA_MISSING";
private string ReportPath = "n/a";
protected override async Task OnInitializedAsync()
{
try
{
var report = await Http.GetFromJsonAsync<OperationalReportData>("api/operational-report");
if (report != null)
{
SchemaVersion = report.SchemaVersion;
SourceJson = report.SourceJson;
GeneratedAt = report.GeneratedAt;
Sections.Clear();
Sections.AddRange(report.Sections);
HighlightSections.Clear();
HighlightSections.AddRange(Sections.Take(4));
SectionCountLabel = report.SectionCount.ToString();
RenderedSectionCountLabel = Sections.Count.ToString();
HealthLabel = Sections.Count > 0 ? "PASS" : "DATA_MISSING";
}
}
catch
{
HealthLabel = "DATA_MISSING";
}
}
}
@@ -1,238 +0,0 @@
@page "/portfolio"
@attribute [Authorize]
@inject HttpClient Http
<PageTitle>QuantEngine - 포트폴리오</PageTitle>
<!-- Page Header -->
<div class="mb-6">
<MudText Typo="Typo.h4" Class="mb-2">포트폴리오</MudText>
<MudText Typo="Typo.body1" Class="text-muted">자산 구성 및 성과 분석</MudText>
</div>
<!-- Summary Cards -->
<MudGrid Spacing="3" Class="mb-6">
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-1">총 평가액</MudText>
<MudText Typo="Typo.h5" Class="text-primary">₩125.5M</MudText>
<MudText Typo="Typo.body2" Class="text-success mt-1">+3.2% (이번 달)</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-1">보유 종목</MudText>
<MudText Typo="Typo.h5">12개</MudText>
<MudText Typo="Typo.body2" Class="text-muted mt-1">주식 및 펀드</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-1">수익률</MudText>
<MudText Typo="Typo.h5" Class="text-success">+8.5%</MudText>
<MudText Typo="Typo.body2" Class="text-muted mt-1">연간 기준</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-1">위험도</MudText>
<MudText Typo="Typo.h5">중간</MudText>
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Warning" Variant="Variant.Filled" Class="mt-1">
Moderate
</MudChip>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Asset Breakdown -->
<MudGrid Spacing="3" Class="mb-6">
<MudItem xs="12" md="8">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">자산 구성</MudText>
<MudTable Items="@_assets" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>종목/펀드명</MudTh>
<MudTh>수량</MudTh>
<MudTh>현재가</MudTh>
<MudTh>평가액</MudTh>
<MudTh>수익률</MudTh>
<MudTh>비율</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">
<div class="d-flex align-items-center gap-2">
<MudAvatar Size="Size.Small" Color="Color.Primary">@context.Name[0]</MudAvatar>
<div>
<MudText Typo="Typo.body2" Class="font-weight-500">@context.Name</MudText>
<MudText Typo="Typo.caption" Class="text-muted">@context.Ticker</MudText>
</div>
</div>
</MudTd>
<MudTd DataLabel="Quantity">
<MudText Typo="Typo.body2">@context.Quantity.ToString("N0")</MudText>
</MudTd>
<MudTd DataLabel="Price">
<MudText Typo="Typo.body2">₩@context.CurrentPrice.ToString("N0")</MudText>
</MudTd>
<MudTd DataLabel="Value">
<MudText Typo="Typo.body2" Class="font-weight-500">₩@context.Value.ToString("N0")</MudText>
</MudTd>
<MudTd DataLabel="Return">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@(context.ReturnRate >= 0 ? Color.Success : Color.Error)"
Variant="Variant.Filled">
@(context.ReturnRate >= 0 ? "+" : "")@context.ReturnRate.ToString("F1")%
</MudChip>
</MudTd>
<MudTd DataLabel="Ratio">
<MudText Typo="Typo.body2">@context.Ratio.ToString("F1")%</MudText>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
<MudItem xs="12" md="4">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">자산 분류</MudText>
<MudStack Spacing="2">
@foreach (var category in AssetCategories)
{
<div>
<div class="d-flex justify-content-between mb-1">
<MudText Typo="Typo.body2">@category.Name</MudText>
<MudText Typo="Typo.body2" Class="font-weight-500">@category.Percentage%</MudText>
</div>
<MudProgressLinear Value="@category.Percentage" Color="@category.Color" />
</div>
}
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Trading History -->
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">거래 이력</MudText>
@if (TradingHistory.Count == 0)
{
<MudAlert Severity="Severity.Info">거래 이력이 없습니다.</MudAlert>
}
else
{
<MudTable Items="@TradingHistory" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>일자</MudTh>
<MudTh>종목</MudTh>
<MudTh>구분</MudTh>
<MudTh>수량</MudTh>
<MudTh>단가</MudTh>
<MudTh>금액</MudTh>
<MudTh>수수료</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Date">
<MudText Typo="Typo.body2">@context.Date.ToString("yyyy-MM-dd")</MudText>
</MudTd>
<MudTd DataLabel="Ticker">
<MudText Typo="Typo.body2">@context.Ticker</MudText>
</MudTd>
<MudTd DataLabel="Type">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@(context.Type == "매수" ? Color.Success : Color.Error)"
Variant="Variant.Filled">
@context.Type
</MudChip>
</MudTd>
<MudTd DataLabel="Quantity">
<MudText Typo="Typo.body2">@context.Quantity</MudText>
</MudTd>
<MudTd DataLabel="Price">
<MudText Typo="Typo.body2">₩@context.Price.ToString("N0")</MudText>
</MudTd>
<MudTd DataLabel="Amount">
<MudText Typo="Typo.body2">₩@context.Amount.ToString("N0")</MudText>
</MudTd>
<MudTd DataLabel="Fee">
<MudText Typo="Typo.body2" Class="text-muted">₩@context.Fee.ToString("N0")</MudText>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
@code {
private List<AssetModel> _assets = new();
private List<CategoryModel> AssetCategories = new();
private List<TradeModel> TradingHistory = new();
protected override async Task OnInitializedAsync()
{
await LoadAssets();
}
private async Task LoadAssets()
{
_assets = new List<AssetModel>
{
new AssetModel { Name = "삼성전자", Ticker = "005930", Quantity = 50, CurrentPrice = 70000, Value = 3500000, ReturnRate = 5.2M, Ratio = 28.0M },
new AssetModel { Name = "LG화학", Ticker = "051910", Quantity = 30, CurrentPrice = 820000, Value = 24600000, ReturnRate = -2.1M, Ratio = 19.6M },
new AssetModel { Name = "현대차", Ticker = "005380", Quantity = 40, CurrentPrice = 245000, Value = 9800000, ReturnRate = 8.5M, Ratio = 7.8M },
new AssetModel { Name = "SK하이닉스", Ticker = "000660", Quantity = 25, CurrentPrice = 105000, Value = 2625000, ReturnRate = 12.3M, Ratio = 2.1M },
new AssetModel { Name = "삼성중공업", Ticker = "010140", Quantity = 60, CurrentPrice = 85000, Value = 5100000, ReturnRate = 3.7M, Ratio = 4.1M },
new AssetModel { Name = "포스코", Ticker = "005490", Quantity = 20, CurrentPrice = 75000, Value = 1500000, ReturnRate = -5.2M, Ratio = 1.2M },
};
AssetCategories = new List<CategoryModel>
{
new CategoryModel { Name = "대형주", Percentage = 45, Color = Color.Primary },
new CategoryModel { Name = "중형주", Percentage = 30, Color = Color.Secondary },
new CategoryModel { Name = "소형주", Percentage = 15, Color = Color.Info },
new CategoryModel { Name = "채권/현금", Percentage = 10, Color = Color.Success }
};
TradingHistory = new List<TradeModel>
{
new TradeModel { Date = DateTime.Now.AddDays(-5), Ticker = "005930", Type = "매수", Quantity = 10, Price = 68000, Amount = 680000, Fee = 1360 },
new TradeModel { Date = DateTime.Now.AddDays(-10), Ticker = "051910", Type = "매도", Quantity = 5, Price = 850000, Amount = 4250000, Fee = 8500 },
new TradeModel { Date = DateTime.Now.AddDays(-15), Ticker = "005380", Type = "매수", Quantity = 20, Price = 240000, Amount = 4800000, Fee = 9600 },
};
await Task.CompletedTask;
}
private class AssetModel
{
public string Name { get; set; }
public string Ticker { get; set; }
public int Quantity { get; set; }
public decimal CurrentPrice { get; set; }
public decimal Value { get; set; }
public decimal ReturnRate { get; set; }
public decimal Ratio { get; set; }
}
private class CategoryModel
{
public string Name { get; set; }
public int Percentage { get; set; }
public Color Color { get; set; }
}
private class TradeModel
{
public DateTime Date { get; set; }
public string Ticker { get; set; }
public string Type { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Amount { get; set; }
public decimal Fee { get; set; }
}
}
@@ -1,268 +0,0 @@
@page "/users"
@attribute [Authorize]
@using MudBlazor
@inject HttpClient Http
@inject ISnackbar Snackbar
@inject IDialogService DialogService
<PageTitle>QuantEngine - 사용자 관리</PageTitle>
<!-- Page Header -->
<div class="mb-6">
<MudText Typo="Typo.h4" Class="mb-2">사용자 관리</MudText>
<MudText Typo="Typo.body1" Class="text-muted">시스템 사용자 및 권한 관리</MudText>
</div>
<!-- Action Bar -->
<div class="d-flex justify-content-between align-items-center mb-4">
<MudTextField @bind-Value="SearchQuery" Placeholder="사용자 검색..."
StartAdornment="@Icons.Material.Filled.Search"
Style="width: 300px;" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenAddUserDialog">
<MudIcon Icon="@Icons.Material.Filled.Add" Class="mr-2" />
새 사용자 추가
</MudButton>
</div>
<!-- Users Table -->
<MudPaper Class="pa-4" Elevation="1">
@if (_users.Count == 0)
{
<MudAlert Severity="Severity.Info">사용자가 없습니다.</MudAlert>
}
else
{
<MudTable Items="@FilteredUsers" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>이름</MudTh>
<MudTh>역할</MudTh>
<MudTh>상태</MudTh>
<MudTh>생성일</MudTh>
<MudTh>수정일</MudTh>
<MudTh>작업</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">
<div class="d-flex align-items-center gap-2">
<MudAvatar Size="Size.Small" Color="Color.Primary">@context.Username[0].ToString().ToUpper()</MudAvatar>
<MudText Typo="Typo.body2">@context.Username</MudText>
</div>
</MudTd>
<MudTd DataLabel="Role">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@(context.Role == "Admin" ? Color.Primary : (context.Role == "Operator" ? Color.Secondary : Color.Default))"
Variant="Variant.Filled">
@context.Role
</MudChip>
</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@(context.IsActive ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(context.IsActive ? "활성" : "비활성")
</MudChip>
</MudTd>
<MudTd DataLabel="Joined">
<MudText Typo="Typo.body2">@FormatDate(context.CreatedAt)</MudText>
</MudTd>
<MudTd DataLabel="Updated">
<MudText Typo="Typo.body2">@FormatDate(context.UpdatedAt)</MudText>
</MudTd>
<MudTd DataLabel="Actions">
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary" OnClick="@(() => EditUser(context))">편집</MudButton>
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Error" OnClick="@(() => DeleteUser(context))">삭제</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
<!-- Add/Edit Dialog -->
<MudDialog @bind-Visible="_dialogVisible" Options="_dialogOptions">
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@(_isEditMode ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="mr-3" />
@(_isEditMode ? "사용자 편집" : "새 사용자 추가")
</MudText>
</TitleContent>
<DialogContent>
<MudForm Model="@_formModel" @ref="_form">
<MudTextField T="string" @bind-Value="_formModel.Username" Label="사용자 ID" Required="true" Disabled="@_isEditMode"
RequiredError="사용자 ID를 입력해 주세요." Class="mb-3" />
<MudTextField T="string" @bind-Value="_formModel.Password" Label="@(_isEditMode ? "새 비밀번호 (미입력시 유지)" : "비밀번호")"
InputType="InputType.Password" Required="@(!_isEditMode)" RequiredError="비밀번호를 입력해 주세요." Class="mb-3" />
<MudSelect T="string" @bind-Value="_formModel.Role" Label="역할 권한" Required="true" Class="mb-3">
<MudSelectItem Value="@("Admin")">Admin (관리자)</MudSelectItem>
<MudSelectItem Value="@("Operator")">Operator (운영자)</MudSelectItem>
<MudSelectItem Value="@("Viewer")">Viewer (조회자)</MudSelectItem>
</MudSelect>
@if (_isEditMode)
{
<MudSwitch T="bool" @bind-Value="_formModel.IsActive" Color="Color.Success" Label="계정 활성화 상태" />
}
</MudForm>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CloseDialog" Class="px-5">취소</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveUser" Class="px-5">저장</MudButton>
</DialogActions>
</MudDialog>
@code {
private List<UserDto> _users = new();
private string SearchQuery = "";
private bool _dialogVisible;
private bool _isEditMode;
private MudForm _form = new();
private UserFormModel _formModel = new();
private DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true, CloseButton = true };
private IEnumerable<UserDto> FilteredUsers
{
get => string.IsNullOrEmpty(SearchQuery)
? _users
: _users.Where(u => u.Username.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
}
protected override async Task OnInitializedAsync()
{
await LoadUsers();
}
private async Task LoadUsers()
{
try
{
// BaseAddress is set to HostEnvironment.BaseAddress by DI in Client/Program.cs.
// Never override it with a hardcoded port.
var res = await Http.GetFromJsonAsync<List<UserDto>>("api/users");
if (res != null)
{
_users = res;
}
}
catch (Exception ex)
{
Snackbar.Add($"사용자 목록 로드 실패: {ex.Message}", Severity.Error);
}
}
private void OpenAddUserDialog()
{
_isEditMode = false;
_formModel = new UserFormModel { Role = "Viewer", IsActive = true };
_dialogVisible = true;
}
private void EditUser(UserDto user)
{
_isEditMode = true;
_formModel = new UserFormModel
{
Username = user.Username,
Role = user.Role,
IsActive = user.IsActive,
Password = "" // Clear password field for security
};
_dialogVisible = true;
}
private async Task DeleteUser(UserDto user)
{
bool? result = await DialogService.ShowMessageBoxAsync(
"사용자 삭제",
$"정말로 사용자 '{user.Username}' 계정을 비활성화하시겠습니까?",
yesText: "비활성화", cancelText: "취소");
if (result == true)
{
try
{
var response = await Http.DeleteAsync($"api/users?username={user.Username}");
if (response.IsSuccessStatusCode)
{
Snackbar.Add("사용자 계정이 비활성화되었습니다.", Severity.Success);
await LoadUsers();
}
else
{
Snackbar.Add("계정 비활성화 작업에 실패했습니다.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"API 에러: {ex.Message}", Severity.Error);
}
}
}
private void CloseDialog()
{
_dialogVisible = false;
}
private async Task SaveUser()
{
await _form.Validate();
if (!_form.IsValid) return;
try
{
HttpResponseMessage response;
if (_isEditMode)
{
response = await Http.PutAsJsonAsync("api/users", _formModel);
}
else
{
response = await Http.PostAsJsonAsync("api/users", _formModel);
}
if (response.IsSuccessStatusCode)
{
Snackbar.Add("사용자 정보가 성공적으로 저장되었습니다.", Severity.Success);
_dialogVisible = false;
await LoadUsers();
}
else
{
var error = await response.Content.ReadAsStringAsync();
Snackbar.Add($"저장 실패: {error}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"API 오류 발생: {ex.Message}", Severity.Error);
}
}
private string FormatDate(string isoString)
{
if (string.IsNullOrWhiteSpace(isoString)) return "-";
if (DateTime.TryParse(isoString, out var dt))
{
return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
}
return isoString;
}
public class UserDto
{
public string Username { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public bool IsActive { get; set; }
public string CreatedAt { get; set; } = string.Empty;
public string UpdatedAt { get; set; } = string.Empty;
}
public class UserFormModel
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string Role { get; set; } = "Viewer";
public bool IsActive { get; set; } = true;
}
}
@@ -1,27 +0,0 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.Authorization;
using QuantEngine.Web.Client.Services;
using QuantEngine.Web.Client.Infrastructure;
using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Register LocalStorage for cross-platform session persistence
builder.Services.AddScoped<LocalStorageService>();
// App State Service (RBAC & global state management)
builder.Services.AddScoped<AppStateService>();
// Authentication setup in WebAssembly client
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
// MudBlazor Services (CRITICAL: Required for Interactive WebAssembly)
builder.Services.AddMudServices();
// HttpClient register (API-First standard)
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<ApiClient>();
await builder.Build().RunAsync();
@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\QuantEngine.Core\QuantEngine.Core.csproj" />
<ProjectReference Include="..\..\QuantEngine.Application\QuantEngine.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0" />
<PackageReference Include="MudBlazor" Version="9.0.0" />
</ItemGroup>
</Project>
@@ -1,8 +0,0 @@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo("login");
}
}
@@ -1,221 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Web.Client.Services;
public class ApiClient
{
private readonly HttpClient _http;
private readonly ILogger<ApiClient> _logger;
public ApiClient(HttpClient http, ILogger<ApiClient> logger)
{
_http = http;
// BaseAddress is set by the DI registration in Client/Program.cs via
// builder.HostEnvironment.BaseAddress — never hardcode a port here.
if (_http.BaseAddress == null)
{
throw new InvalidOperationException(
"ApiClient: HttpClient.BaseAddress is null. " +
"Ensure the HttpClient is registered with HostEnvironment.BaseAddress in Client/Program.cs.");
}
_logger = logger;
}
// Collection API Methods
public async Task<CollectionDashboardStateDto?> GetCollectionStateAsync()
{
try
{
return await _http.GetFromJsonAsync<CollectionDashboardStateDto>("api/collection/state");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching collection state");
return null;
}
}
public async Task<CollectionRunsResponse?> GetCollectionRunsAsync(int limit = 20)
{
try
{
return await _http.GetFromJsonAsync<CollectionRunsResponse>($"api/collection/runs?limit={limit}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching collection runs");
return null;
}
}
public async Task<CollectionRunSnapshotsResponse?> GetCollectionSnapshotsAsync(string runId)
{
try
{
return await _http.GetFromJsonAsync<CollectionRunSnapshotsResponse>($"api/collection/runs/{runId}/snapshots");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error fetching snapshots for run {runId}");
return null;
}
}
public async Task<CollectionRunErrorsResponse?> GetCollectionErrorsAsync(string runId, int limit = 50)
{
try
{
return await _http.GetFromJsonAsync<CollectionRunErrorsResponse>($"api/collection/runs/{runId}/errors?limit={limit}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error fetching errors for run {runId}");
return null;
}
}
public async Task<CollectionRunStartResponse?> StartCollectionRunAsync()
{
try
{
var response = await _http.PostAsJsonAsync("api/collection/run", new { });
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<CollectionRunStartResponse>();
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting collection run");
return null;
}
}
}
// DTOs
public class CollectionDashboardStateDto
{
[JsonPropertyName("lastRunId")]
public string? LastRunId { get; set; }
[JsonPropertyName("lastRunStatus")]
public string? LastRunStatus { get; set; }
[JsonPropertyName("lastFinishedAt")]
public string? LastFinishedAt { get; set; }
[JsonPropertyName("totalSnapshots")]
public int TotalSnapshots { get; set; }
[JsonPropertyName("totalErrors")]
public int TotalErrors { get; set; }
[JsonPropertyName("recentErrors")]
public List<CollectionErrorDto> RecentErrors { get; set; } = new();
}
public class CollectionRunDto
{
[JsonPropertyName("runId")]
public string RunId { get; set; } = "";
[JsonPropertyName("status")]
public string Status { get; set; } = "";
[JsonPropertyName("startedAt")]
public string StartedAt { get; set; } = "";
[JsonPropertyName("finishedAt")]
public string? FinishedAt { get; set; }
[JsonPropertyName("totalSnapshots")]
public int? TotalSnapshots { get; set; }
[JsonPropertyName("totalErrors")]
public int? TotalErrors { get; set; }
}
public class CollectionSnapshotDto
{
[JsonPropertyName("runId")]
public string RunId { get; set; } = "";
[JsonPropertyName("datasetName")]
public string DatasetName { get; set; } = "";
[JsonPropertyName("ticker")]
public string Ticker { get; set; } = "";
[JsonPropertyName("sourceName")]
public string SourceName { get; set; } = "";
[JsonPropertyName("capturedAt")]
public string CapturedAt { get; set; } = "";
}
public class CollectionErrorDto
{
[JsonPropertyName("runId")]
public string RunId { get; set; } = "";
[JsonPropertyName("sourceName")]
public string SourceName { get; set; } = "";
[JsonPropertyName("errorKind")]
public string ErrorKind { get; set; } = "";
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; } = "";
[JsonPropertyName("ticker")]
public string Ticker { get; set; } = "";
}
public class CollectionRunsResponse
{
[JsonPropertyName("runs")]
public List<CollectionRunDto> Runs { get; set; } = new();
[JsonPropertyName("count")]
public int Count { get; set; }
}
public class CollectionRunSnapshotsResponse
{
[JsonPropertyName("runId")]
public string RunId { get; set; } = "";
[JsonPropertyName("snapshots")]
public List<CollectionSnapshotDto> Snapshots { get; set; } = new();
[JsonPropertyName("count")]
public int Count { get; set; }
}
public class CollectionRunErrorsResponse
{
[JsonPropertyName("runId")]
public string RunId { get; set; } = "";
[JsonPropertyName("errors")]
public List<CollectionErrorDto> Errors { get; set; } = new();
[JsonPropertyName("count")]
public int Count { get; set; }
}
public class CollectionRunStartResponse
{
[JsonPropertyName("runId")]
public string RunId { get; set; } = "";
[JsonPropertyName("status")]
public string Status { get; set; } = "";
[JsonPropertyName("startedAt")]
public string StartedAt { get; set; } = "";
}
@@ -1,142 +0,0 @@
namespace QuantEngine.Web.Client.Services;
public class AppStateService
{
private UserContext _currentUser;
private List<string> _userRoles = new();
private bool _isInitialized = false;
public event Action OnStateChanged;
public UserContext CurrentUser
{
get => _currentUser;
set
{
_currentUser = value;
NotifyStateChanged();
}
}
public List<string> UserRoles
{
get => _userRoles;
set
{
_userRoles = value;
NotifyStateChanged();
}
}
public bool IsInitialized
{
get => _isInitialized;
set
{
_isInitialized = value;
NotifyStateChanged();
}
}
public AppStateService()
{
_currentUser = new UserContext();
_userRoles = new List<string>();
}
/// <summary>
/// Initialize app state from current user context
/// </summary>
public async Task InitializeAsync(HttpClient httpClient)
{
try
{
var response = await httpClient.GetAsync("api/auth/user");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
// Parse user info (implement as needed)
CurrentUser = new UserContext { Name = "Admin", Email = "admin@quantengine.local" };
UserRoles = new List<string> { "Admin" };
}
}
catch
{
// Handle error
}
finally
{
IsInitialized = true;
}
}
/// <summary>
/// Check if user has specific role (RBAC)
/// </summary>
public bool HasRole(string role)
{
return UserRoles.Contains(role);
}
/// <summary>
/// Check if user has any of the specified roles
/// </summary>
public bool HasAnyRole(params string[] roles)
{
return roles.Any(r => UserRoles.Contains(r));
}
/// <summary>
/// Check if user has all specified roles
/// </summary>
public bool HasAllRoles(params string[] roles)
{
return roles.All(r => UserRoles.Contains(r));
}
/// <summary>
/// Clear user state
/// </summary>
public void Clear()
{
CurrentUser = new UserContext();
UserRoles = new List<string>();
IsInitialized = false;
}
private void NotifyStateChanged() => OnStateChanged?.Invoke();
}
/// <summary>
/// User context model
/// </summary>
public class UserContext
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.Now;
public bool IsActive { get; set; } = true;
}
/// <summary>
/// API Response wrapper
/// </summary>
public class ApiResponse<T>
{
public bool Success { get; set; }
public string Message { get; set; }
public T Data { get; set; }
}
/// <summary>
/// Pagination model
/// </summary>
public class PaginatedResponse<T>
{
public List<T> Items { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
}
@@ -1,43 +0,0 @@
using Microsoft.JSInterop;
using System.Text.Json;
namespace QuantEngine.Web.Client.Services
{
public class LocalStorageService
{
private readonly IJSRuntime _js;
public LocalStorageService(IJSRuntime js)
{
_js = js;
}
public async Task SetAsync<T>(string key, T value)
{
var json = JsonSerializer.Serialize(value);
await _js.InvokeVoidAsync("localStorage.setItem", key, json);
}
public async Task<T?> GetAsync<T>(string key)
{
try
{
var json = await _js.InvokeAsync<string?>("localStorage.getItem", key);
if (string.IsNullOrEmpty(json))
{
return default;
}
return JsonSerializer.Deserialize<T>(json);
}
catch
{
return default;
}
}
public async Task DeleteAsync(string key)
{
await _js.InvokeVoidAsync("localStorage.removeItem", key);
}
}
}
@@ -1,158 +0,0 @@
using MudBlazor;
namespace QuantEngine.Web.Client.Theme;
public static class AppTheme
{
public static MudTheme LightTheme => new()
{
PaletteLight = new PaletteLight
{
Primary = "#3f51b5",
Secondary = "#f50057",
Success = "#4caf50",
Warning = "#ff9800",
Error = "#f44336",
Info = "#2196f3",
Dark = "#121212",
Background = "#fafafa",
Surface = "#ffffff",
TextPrimary = "#212121",
TextSecondary = "rgba(0,0,0,0.6)",
DrawerBackground = "#ffffff",
DrawerText = "#212121",
AppbarBackground = "#3f51b5",
AppbarText = "#ffffff",
ActionDefault = "#c0c0c0",
ActionDisabled = "#f5f5f5",
ActionDisabledBackground = "rgba(0,0,0,0.12)",
Divider = "#e0e0e0",
DividerLight = "#f5f5f5",
TableLines = "#e0e0e0",
LinesDefault = "#e0e0e0",
LinesInputs = "#bdbdbd",
TextDisabled = "rgba(0,0,0,0.38)"
},
Typography = new Typography
{
Default = new DefaultTypography
{
FontFamily = new[] { "Roboto", "sans-serif" },
FontSize = "1rem",
FontWeight = "400",
LineHeight = "1.5",
LetterSpacing = "0.5px"
},
H1 = new H1Typography
{
FontSize = "6rem",
FontWeight = "300",
LineHeight = "1.167",
LetterSpacing = "-0.015625em"
},
H2 = new H2Typography
{
FontSize = "3.75rem",
FontWeight = "300",
LineHeight = "1.2",
LetterSpacing = "-0.0083333333em"
},
H3 = new H3Typography
{
FontSize = "3rem",
FontWeight = "400",
LineHeight = "1.167",
LetterSpacing = "0em"
},
H4 = new H4Typography
{
FontSize = "2.125rem",
FontWeight = "500",
LineHeight = "1.235",
LetterSpacing = "0.0125em"
},
H5 = new H5Typography
{
FontSize = "1.5rem",
FontWeight = "500",
LineHeight = "1.334",
LetterSpacing = "0em"
},
H6 = new H6Typography
{
FontSize = "1.25rem",
FontWeight = "600",
LineHeight = "1.6",
LetterSpacing = "0.0125em"
},
Body1 = new Body1Typography
{
FontSize = "1rem",
FontWeight = "500",
LineHeight = "1.5",
LetterSpacing = "0.03125em"
},
Body2 = new Body2Typography
{
FontSize = "0.875rem",
FontWeight = "400",
LineHeight = "1.43",
LetterSpacing = "0.0178571429em"
},
Button = new ButtonTypography
{
FontSize = "0.875rem",
FontWeight = "600",
LineHeight = "1.75",
LetterSpacing = "0.0892857143em"
},
Caption = new CaptionTypography
{
FontSize = "0.75rem",
FontWeight = "400",
LineHeight = "1.66",
LetterSpacing = "0.0333333333em"
}
},
LayoutProperties = new LayoutProperties
{
DefaultBorderRadius = "4px",
DrawerWidthLeft = "256px",
DrawerWidthRight = "256px",
AppbarHeight = "64px",
}
};
public static MudTheme DarkTheme => new()
{
PaletteDark = new PaletteDark
{
Primary = "#bb86fc",
Secondary = "#03dac6",
Success = "#4caf50",
Warning = "#ff9800",
Error = "#cf6679",
Info = "#2196f3",
Dark = "#121212",
Background = "#121212",
Surface = "#1e1e1e",
TextPrimary = "#ffffff",
TextSecondary = "rgba(255,255,255,0.7)",
DrawerBackground = "#1e1e1e",
DrawerText = "#ffffff",
AppbarBackground = "#1f1f1f",
AppbarText = "#ffffff",
ActionDefault = "#3f3f3f",
ActionDisabled = "#1e1e1e",
ActionDisabledBackground = "rgba(255,255,255,0.12)",
Divider = "#37474f",
DividerLight = "#2c3e50",
TableLines = "#37474f",
LinesDefault = "#37474f",
LinesInputs = "#555555",
TextDisabled = "rgba(255,255,255,0.38)"
},
Typography = LightTheme.Typography,
LayoutProperties = LightTheme.LayoutProperties
};
}
@@ -1,16 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MudBlazor
@using QuantEngine.Web.Client
@using QuantEngine.Web.Client.Pages
@using QuantEngine.Web.Client.Layout
@using QuantEngine.Web.Client.Infrastructure
@using QuantEngine.Web.Client.Services
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
-297
View File
@@ -1,297 +0,0 @@
/* QuantEngine Global Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 1.5;
color: var(--mud-palette-text-primary, #212121);
background-color: var(--mud-palette-background, #fafafa);
transition: background-color 0.3s ease, color 0.3s ease;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--mud-palette-surface, #ffffff);
}
::-webkit-scrollbar-thumb {
background: var(--mud-palette-action-default, #c0c0c0);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--mud-palette-primary, #3f51b5);
}
/* Text Utilities */
.text-primary {
color: var(--mud-palette-primary, #3f51b5);
}
.text-secondary {
color: var(--mud-palette-secondary, #f50057);
}
.text-success {
color: var(--mud-palette-success, #4caf50);
}
.text-warning {
color: var(--mud-palette-warning, #ff9800);
}
.text-error {
color: var(--mud-palette-error, #f44336);
}
.text-muted {
color: var(--mud-palette-text-secondary, rgba(0,0,0,0.6));
}
/* Spacing Utilities */
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.mt-4 { margin-top: 1.5rem; }
.mt-5 { margin-top: 3rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mb-5 { margin-bottom: 3rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.my-auto { margin-top: auto; margin-bottom: auto; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
/* Flex Utilities */
.d-flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.align-items-center {
align-items: center;
}
.justify-content-center {
justify-content: center;
}
.justify-content-between {
justify-content: space-between;
}
/* Gap Utilities */
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 1rem; }
.gap-4 { gap: 1.5rem; }
/* Loading Skeleton */
.skeleton {
background: linear-gradient(
90deg,
var(--mud-palette-surface, #fff) 0%,
var(--mud-palette-divider, #e0e0e0) 50%,
var(--mud-palette-surface, #fff) 100%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* MudBlazor Overrides */
.mud-appbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.mud-drawer {
border-right: 1px solid var(--mud-palette-divider, #e0e0e0);
}
.mud-drawer-content {
padding: 1rem;
}
.mud-nav-link {
border-radius: 4px;
margin-bottom: 0.25rem;
transition: all 0.2s ease;
}
.mud-nav-link:hover {
background-color: var(--mud-palette-action-default-hover, rgba(0, 0, 0, 0.04));
}
.mud-nav-link.mud-ripple-nav-link-active {
background-color: var(--mud-palette-primary-lighten, rgba(63, 81, 181, 0.1));
color: var(--mud-palette-primary, #3f51b5);
font-weight: 600;
}
.mud-card {
border: 1px solid var(--mud-palette-divider, #e0e0e0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.mud-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.mud-button {
text-transform: none;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 4px;
}
.mud-button-root:disabled {
opacity: 0.6;
}
/* Forms */
.mud-input-control {
margin-bottom: 1rem;
}
.mud-input-label {
font-weight: 500;
}
.mud-input {
border-radius: 4px;
}
.mud-input.mud-input-text {
background-color: var(--mud-palette-surface, #ffffff);
}
/* Tables */
.mud-table {
background-color: var(--mud-palette-surface, #ffffff);
}
.mud-table-head {
background-color: var(--mud-palette-background, #fafafa);
}
.mud-table-row:hover {
background-color: var(--mud-palette-action-default-hover, rgba(0, 0, 0, 0.04));
}
.mud-table-cell {
padding: 1rem;
border-color: var(--mud-palette-divider, #e0e0e0);
}
/* Responsive */
@media (max-width: 600px) {
body {
font-size: 13px;
}
.mud-drawer {
width: 100% !important;
max-width: 90% !important;
}
.mud-appbar {
height: 56px;
}
.mud-table-cell {
padding: 0.75rem 0.5rem;
}
}
/* Animation Classes */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.slide-in {
animation: slideIn 0.3s ease-in;
}
@keyframes slideIn {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Accessibility */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Print Styles */
@media print {
.mud-appbar,
.mud-drawer,
.no-print {
display: none !important;
}
body {
background: white;
}
}
@@ -18,25 +18,10 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<!-- Exclude client project files from server build to avoid duplicate compilations -->
<!-- BUT preserve Client\wwwroot for static web assets -->
<Compile Remove="Client\**" />
<EmbeddedResource Remove="Client\**" />
<None Remove="Client\**" />
</ItemGroup>
<ItemGroup>
<!-- Only remove non-wwwroot Client content -->
<Content Remove="Client\**" />
<Content Include="Client\wwwroot\**" CopyToPublishDirectory="Never" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
</Project>
@@ -1,18 +0,0 @@
2026-07-11 17:02:14.217 +09:00 [FTL] Application terminated unexpectedly
System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: QuantEngine.Core.Interfaces.IKisApiClient Lifetime: Scoped ImplementationType: QuantEngine.Infrastructure.Services.KisApiClient': Unable to resolve service for type 'System.Net.Http.HttpClient' while attempting to activate 'QuantEngine.Infrastructure.Services.KisApiClient'.)
---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: QuantEngine.Core.Interfaces.IKisApiClient Lifetime: Scoped ImplementationType: QuantEngine.Infrastructure.Services.KisApiClient': Unable to resolve service for type 'System.Net.Http.HttpClient' while attempting to activate 'QuantEngine.Infrastructure.Services.KisApiClient'.
---> System.InvalidOperationException: Unable to resolve service for type 'System.Net.Http.HttpClient' while attempting to activate 'QuantEngine.Infrastructure.Services.KisApiClient'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceDescriptor serviceDescriptor, CallSiteChain callSiteChain)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
--- End of inner exception stack trace ---
at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
--- End of inner exception stack trace ---
at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
at Program.<Main>$(String[] args) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Program.cs:line 99
@@ -0,0 +1,381 @@
2026-07-11 18:41:46.431 +09:00 [INF] Registered 10 endpoints in 164 milliseconds.
2026-07-11 18:41:46.473 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:41:50.463 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:41:50.464 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:41:50.476 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:41:50.498 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:42:27.125 +09:00 [INF] Registered 10 endpoints in 178 milliseconds.
2026-07-11 18:42:27.164 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:42:31.234 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:42:31.234 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:42:31.240 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:42:31.281 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:42:35.322 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-11 18:42:35.536 +09:00 [ERR] Hosting failed to start
System.IO.IOException: Failed to bind to address http://127.0.0.1:5265: address already in use.
---> Microsoft.AspNetCore.Connections.AddressInUseException: 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
---> System.Net.Sockets.SocketException (10048): 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, Boolean disconnectOnFailure, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(EndPoint endpoint)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.BindAsync(EndPoint endpoint, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TransportManager.BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig endpointConfig, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.<>c__DisplayClass28_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.BindAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
2026-07-11 18:42:35.543 +09:00 [FTL] Application terminated unexpectedly
System.IO.IOException: Failed to bind to address http://127.0.0.1:5265: address already in use.
---> Microsoft.AspNetCore.Connections.AddressInUseException: 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
---> System.Net.Sockets.SocketException (10048): 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, Boolean disconnectOnFailure, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(EndPoint endpoint)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.BindAsync(EndPoint endpoint, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TransportManager.BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig endpointConfig, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.<>c__DisplayClass28_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.BindAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
at Program.<Main>$(String[] args) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Program.cs:line 172
2026-07-11 18:47:26.979 +09:00 [INF] Registered 10 endpoints in 173 milliseconds.
2026-07-11 18:47:27.018 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:47:31.144 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:47:31.144 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:47:31.149 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:47:31.177 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:47:58.996 +09:00 [INF] Registered 10 endpoints in 163 milliseconds.
2026-07-11 18:47:59.034 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:48:03.135 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:48:03.136 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:48:03.142 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:48:03.166 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:48:07.181 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-11 18:48:07.453 +09:00 [ERR] Hosting failed to start
System.IO.IOException: Failed to bind to address http://127.0.0.1:5265: address already in use.
---> Microsoft.AspNetCore.Connections.AddressInUseException: 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
---> System.Net.Sockets.SocketException (10048): 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, Boolean disconnectOnFailure, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(EndPoint endpoint)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.BindAsync(EndPoint endpoint, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TransportManager.BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig endpointConfig, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.<>c__DisplayClass28_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.BindAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
2026-07-11 18:48:07.460 +09:00 [FTL] Application terminated unexpectedly
System.IO.IOException: Failed to bind to address http://127.0.0.1:5265: address already in use.
---> Microsoft.AspNetCore.Connections.AddressInUseException: 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
---> System.Net.Sockets.SocketException (10048): 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, Boolean disconnectOnFailure, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(EndPoint endpoint)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.BindAsync(EndPoint endpoint, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TransportManager.BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig endpointConfig, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.<>c__DisplayClass28_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.BindAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
at Program.<Main>$(String[] args) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Program.cs:line 172
2026-07-11 18:49:42.328 +09:00 [INF] Registered 10 endpoints in 160 milliseconds.
2026-07-11 18:49:42.368 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:49:46.632 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:49:46.632 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:49:46.635 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:49:46.655 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:49:50.782 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-11 18:49:50.940 +09:00 [INF] Now listening on: http://localhost:5265
2026-07-11 18:49:50.946 +09:00 [INF] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage'
2026-07-11 18:49:50.946 +09:00 [INF] Using the following options for Hangfire Server:
Worker count: 32
Listening queues: 'default'
Shutdown timeout: 00:00:15
Schedule polling interval: 00:00:15
2026-07-11 18:49:50.966 +09:00 [INF] Application started. Press Ctrl+C to shut down.
2026-07-11 18:49:50.966 +09:00 [INF] Hosting environment: Development
2026-07-11 18:49:50.966 +09:00 [INF] Content root path: C:\Temp\data_feed\src\dotnet\QuantEngine.Web
2026-07-11 18:49:51.046 +09:00 [INF] Server kimjaehyun-note:18540:24fc9869 successfully announced in 77.2082 ms
2026-07-11 18:49:51.050 +09:00 [INF] Server kimjaehyun-note:18540:24fc9869 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2026-07-11 18:49:51.076 +09:00 [INF] Server kimjaehyun-note:18540:24fc9869 all the dispatchers started
2026-07-11 18:50:18.938 +09:00 [INF] Registered 10 endpoints in 161 milliseconds.
2026-07-11 18:50:18.977 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:50:23.249 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:50:23.250 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:50:23.256 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:50:23.283 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:50:27.388 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-11 18:50:27.569 +09:00 [INF] Now listening on: http://localhost:5265
2026-07-11 18:50:27.577 +09:00 [INF] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage'
2026-07-11 18:50:27.577 +09:00 [INF] Using the following options for Hangfire Server:
Worker count: 32
Listening queues: 'default'
Shutdown timeout: 00:00:15
Schedule polling interval: 00:00:15
2026-07-11 18:50:27.589 +09:00 [INF] Application started. Press Ctrl+C to shut down.
2026-07-11 18:50:27.590 +09:00 [INF] Hosting environment: Development
2026-07-11 18:50:27.590 +09:00 [INF] Content root path: C:\Temp\data_feed\src\dotnet\QuantEngine.Web
2026-07-11 18:50:27.646 +09:00 [INF] Server kimjaehyun-note:25888:2b65733c successfully announced in 54.5404 ms
2026-07-11 18:50:27.648 +09:00 [INF] Server kimjaehyun-note:25888:2b65733c is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2026-07-11 18:50:27.667 +09:00 [INF] Server kimjaehyun-note:25888:2b65733c all the dispatchers started
2026-07-11 18:50:40.967 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 18:50:40.972 +09:00 [WRN] Failed to determine the https port for redirect.
2026-07-11 18:50:41.007 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 18:50:41.023 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 18:50:41.030 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 18:50:41.031 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 18:50:41.033 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 18:50:41.033 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 18:50:41.058 +09:00 [INF] Executed page /Account/Login in 32.7802ms
2026-07-11 18:50:41.059 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 18:50:41.060 +09:00 [INF] HTTP GET /Account/Login responded 200 in 88.7566 ms
2026-07-11 18:50:41.062 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 94.7893ms
2026-07-11 18:50:41.174 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-11 18:50:41.178 +09:00 [ERR] HTTP GET /Admin/Dashboard responded 500 in 4.0554 ms
System.InvalidOperationException: The AuthorizationPolicy named: 'AdminCookie' was not found.
at Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable`1 authorizeData, IEnumerable`1 policies)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
2026-07-11 18:50:41.181 +09:00 [ERR] An unhandled exception has occurred while executing the request.
System.InvalidOperationException: The AuthorizationPolicy named: 'AdminCookie' was not found.
at Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable`1 authorizeData, IEnumerable`1 policies)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
2026-07-11 18:50:41.185 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 500 null text/plain; charset=utf-8 12.0749ms
2026-07-11 18:50:41.298 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 18:50:41.303 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 18:50:41.303 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 18:50:41.304 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 18:50:41.304 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 18:50:41.304 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 18:50:41.304 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 18:50:41.310 +09:00 [INF] Executed page /Account/Login in 6.8704ms
2026-07-11 18:50:41.310 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 18:50:41.310 +09:00 [INF] HTTP GET /Account/Login responded 200 in 12.5730 ms
2026-07-11 18:50:41.311 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 12.9475ms
2026-07-11 18:50:45.183 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-11 18:50:45.185 +09:00 [ERR] HTTP GET /Admin/Dashboard responded 500 in 1.8220 ms
System.InvalidOperationException: The AuthorizationPolicy named: 'AdminCookie' was not found.
at Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable`1 authorizeData, IEnumerable`1 policies)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
2026-07-11 18:50:45.186 +09:00 [ERR] An unhandled exception has occurred while executing the request.
System.InvalidOperationException: The AuthorizationPolicy named: 'AdminCookie' was not found.
at Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable`1 authorizeData, IEnumerable`1 policies)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
2026-07-11 18:50:45.186 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 500 null text/plain; charset=utf-8 2.8989ms
2026-07-11 18:51:53.325 +09:00 [INF] Registered 10 endpoints in 159 milliseconds.
2026-07-11 18:51:53.369 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-11 18:51:57.672 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:51:57.672 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-11 18:51:57.699 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-11 18:51:57.728 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-11 18:52:01.841 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-11 18:52:02.012 +09:00 [INF] Now listening on: http://localhost:5265
2026-07-11 18:52:02.023 +09:00 [INF] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage'
2026-07-11 18:52:02.024 +09:00 [INF] Using the following options for Hangfire Server:
Worker count: 32
Listening queues: 'default'
Shutdown timeout: 00:00:15
Schedule polling interval: 00:00:15
2026-07-11 18:52:02.041 +09:00 [INF] Application started. Press Ctrl+C to shut down.
2026-07-11 18:52:02.041 +09:00 [INF] Hosting environment: Development
2026-07-11 18:52:02.041 +09:00 [INF] Content root path: C:\Temp\data_feed\src\dotnet\QuantEngine.Web
2026-07-11 18:52:02.106 +09:00 [INF] Server kimjaehyun-note:28380:a220bb7a successfully announced in 61.5885 ms
2026-07-11 18:52:02.108 +09:00 [INF] Server kimjaehyun-note:28380:a220bb7a is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2026-07-11 18:52:02.115 +09:00 [INF] Server kimjaehyun-note:28380:a220bb7a all the dispatchers started
2026-07-11 18:52:13.775 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 18:52:13.780 +09:00 [WRN] Failed to determine the https port for redirect.
2026-07-11 18:52:13.833 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 18:52:13.849 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 18:52:13.855 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 18:52:13.856 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 18:52:13.858 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 18:52:13.859 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 18:52:13.883 +09:00 [INF] Executed page /Account/Login in 32.0627ms
2026-07-11 18:52:13.884 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 18:52:13.885 +09:00 [INF] HTTP GET /Account/Login responded 200 in 106.3802 ms
2026-07-11 18:52:13.887 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 112.9483ms
2026-07-11 18:52:14.003 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-11 18:52:14.011 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.013 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.013 +09:00 [INF] HTTP GET /Admin/Dashboard responded 302 in 10.4176 ms
2026-07-11 18:52:14.014 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 302 0 null 11.3635ms
2026-07-11 18:52:14.120 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-11 18:52:14.124 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.124 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.125 +09:00 [INF] HTTP GET /Admin/Dashboard responded 302 in 4.4516 ms
2026-07-11 18:52:14.125 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 302 0 null 4.7369ms
2026-07-11 18:52:14.308 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-11 18:52:14.309 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.310 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.310 +09:00 [INF] HTTP GET /Admin/Dashboard responded 302 in 1.4155 ms
2026-07-11 18:52:14.310 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 302 0 null 1.7032ms
2026-07-11 18:52:14.410 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Users - null null
2026-07-11 18:52:14.411 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.411 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.411 +09:00 [INF] HTTP GET /Admin/Users responded 302 in 0.4739 ms
2026-07-11 18:52:14.411 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Users - 302 0 null 0.7314ms
2026-07-11 18:52:14.523 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Collection - null null
2026-07-11 18:52:14.523 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.523 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.524 +09:00 [INF] HTTP GET /Admin/Collection responded 302 in 0.6342 ms
2026-07-11 18:52:14.524 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Collection - 302 0 null 1.6071ms
2026-07-11 18:52:14.642 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Monitoring - null null
2026-07-11 18:52:14.643 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.643 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.643 +09:00 [INF] HTTP GET /Admin/Monitoring responded 302 in 0.4305 ms
2026-07-11 18:52:14.643 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Monitoring - 302 0 null 0.677ms
2026-07-11 18:52:14.741 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Operations - null null
2026-07-11 18:52:14.741 +09:00 [INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
2026-07-11 18:52:14.742 +09:00 [INF] AuthenticationScheme: AdminCookie was challenged.
2026-07-11 18:52:14.742 +09:00 [INF] HTTP GET /Admin/Operations responded 302 in 0.4306 ms
2026-07-11 18:52:14.742 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Operations - 302 0 null 0.6763ms
2026-07-11 21:04:06.174 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-11 21:04:06.177 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-11 21:04:06.177 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-11 21:04:06.178 +09:00 [INF] HTTP GET /login responded 302 in 2.1715 ms
2026-07-11 21:04:06.178 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 3.7332ms
2026-07-11 21:04:06.191 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 21:04:06.194 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 21:04:06.195 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 21:04:06.198 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 21:04:06.199 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 21:04:06.199 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 21:04:06.200 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 21:04:06.212 +09:00 [INF] Executed page /Account/Login in 16.6942ms
2026-07-11 21:04:06.212 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 21:04:06.212 +09:00 [INF] HTTP GET /Account/Login responded 200 in 21.4926 ms
2026-07-11 21:04:06.213 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 22.1426ms
2026-07-11 21:04:06.222 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-11 21:04:06.223 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-11 21:04:06.223 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-11 21:04:06.223 +09:00 [INF] HTTP GET /login responded 302 in 0.5991 ms
2026-07-11 21:04:06.223 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 1.2095ms
2026-07-11 21:04:06.226 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 21:04:06.227 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 21:04:06.227 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 21:04:06.231 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 21:04:06.232 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 21:04:06.232 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 21:04:06.233 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 21:04:06.238 +09:00 [INF] Executed page /Account/Login in 10.6054ms
2026-07-11 21:04:06.238 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 21:04:06.238 +09:00 [INF] HTTP GET /Account/Login responded 200 in 11.7060 ms
2026-07-11 21:04:06.238 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 12.2449ms
2026-07-11 21:05:05.597 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-11 21:05:05.597 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-11 21:05:05.598 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-11 21:05:05.598 +09:00 [INF] HTTP GET /login responded 302 in 0.4316 ms
2026-07-11 21:05:05.598 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 0.8539ms
2026-07-11 21:05:05.606 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 21:05:05.606 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 21:05:05.606 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 21:05:05.606 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 21:05:05.606 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 21:05:05.607 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 21:05:05.607 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 21:05:05.607 +09:00 [INF] Executed page /Account/Login in 1.1238ms
2026-07-11 21:05:05.607 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 21:05:05.607 +09:00 [INF] HTTP GET /Account/Login responded 200 in 1.6523 ms
2026-07-11 21:05:05.607 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 1.9459ms
2026-07-11 21:05:05.611 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-11 21:05:05.612 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-11 21:05:05.612 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-11 21:05:05.612 +09:00 [INF] HTTP GET /login responded 302 in 0.3471 ms
2026-07-11 21:05:05.612 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 0.603ms
2026-07-11 21:05:05.616 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-11 21:05:05.616 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-11 21:05:05.616 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-11 21:05:05.616 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-11 21:05:05.616 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-11 21:05:05.616 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-11 21:05:05.616 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-11 21:05:05.617 +09:00 [INF] Executed page /Account/Login in 0.5619ms
2026-07-11 21:05:05.617 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-11 21:05:05.617 +09:00 [INF] HTTP GET /Account/Login responded 200 in 0.9286 ms
2026-07-11 21:05:05.617 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 1.1671ms
@@ -0,0 +1,610 @@
2026-07-12 01:49:59.393 +09:00 [INF] Registered 10 endpoints in 432 milliseconds.
2026-07-12 01:49:59.756 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-12 01:50:02.068 +09:00 [INF] ✅ Database migration completed successfully
2026-07-12 01:50:05.349 +09:00 [INF] Database migration and initialization successful
2026-07-12 01:50:05.388 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-12 01:50:06.028 +09:00 [INF] Start installing Hangfire SQL objects...
2026-07-12 01:50:10.702 +09:00 [INF] Hangfire SQL objects installed.
2026-07-12 01:50:10.728 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-12 01:50:10.877 +09:00 [ERR] Hosting failed to start
System.IO.IOException: Failed to bind to address http://127.0.0.1:5265: address already in use.
---> Microsoft.AspNetCore.Connections.AddressInUseException: 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
---> System.Net.Sockets.SocketException (10048): 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, Boolean disconnectOnFailure, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(EndPoint endpoint)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.BindAsync(EndPoint endpoint, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TransportManager.BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig endpointConfig, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.<>c__DisplayClass28_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.BindAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
2026-07-12 01:50:10.905 +09:00 [FTL] Application terminated unexpectedly
System.IO.IOException: Failed to bind to address http://127.0.0.1:5265: address already in use.
---> Microsoft.AspNetCore.Connections.AddressInUseException: 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
---> System.Net.Sockets.SocketException (10048): 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, Boolean disconnectOnFailure, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(EndPoint endpoint)
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketConnectionListener.Bind()
at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.BindAsync(EndPoint endpoint, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TransportManager.BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig endpointConfig, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.<>c__DisplayClass28_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.BindAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerImpl.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
at Program.<Main>$(String[] args) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Program.cs:line 178
2026-07-12 01:50:55.147 +09:00 [INF] Registered 10 endpoints in 161 milliseconds.
2026-07-12 01:50:55.188 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-12 01:50:57.488 +09:00 [INF] ✅ Database migration completed successfully
2026-07-12 01:51:00.589 +09:00 [INF] Database migration and initialization successful
2026-07-12 01:51:00.610 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-12 01:51:00.632 +09:00 [INF] Start installing Hangfire SQL objects...
2026-07-12 01:51:05.254 +09:00 [INF] Hangfire SQL objects installed.
2026-07-12 01:51:05.261 +09:00 [WRN] Hangfire setup failed: Cannot resolve scoped service 'QuantEngine.Web.Services.SchedulerService' from root provider.
2026-07-12 01:51:05.409 +09:00 [INF] Now listening on: http://localhost:5265
2026-07-12 01:51:05.417 +09:00 [INF] Starting Hangfire Server using job storage: 'PostgreSQL Server: Host: 127.0.0.1, DB: quantenginedb, Schema: hangfire'
2026-07-12 01:51:05.417 +09:00 [INF] Using the following options for PostgreSQL job storage:
2026-07-12 01:51:05.418 +09:00 [INF] Queue poll interval: 00:00:15.
2026-07-12 01:51:05.418 +09:00 [INF] Invisibility timeout: 00:30:00.
2026-07-12 01:51:05.418 +09:00 [INF] Use sliding invisibility timeout: False.
2026-07-12 01:51:05.418 +09:00 [INF] Using the following options for Hangfire Server:
Worker count: 32
Listening queues: 'default'
Shutdown timeout: 00:00:15
Schedule polling interval: 00:00:15
2026-07-12 01:51:05.434 +09:00 [INF] Application started. Press Ctrl+C to shut down.
2026-07-12 01:51:05.434 +09:00 [INF] Hosting environment: Development
2026-07-12 01:51:05.434 +09:00 [INF] Content root path: C:\Temp\data_feed\src\dotnet\QuantEngine.Web
2026-07-12 01:51:05.741 +09:00 [INF] Server kimjaehyun-note:5816:6e6d8857 successfully announced in 304.3044 ms
2026-07-12 01:51:05.744 +09:00 [INF] Server kimjaehyun-note:5816:6e6d8857 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2026-07-12 01:51:05.762 +09:00 [INF] Server kimjaehyun-note:5816:6e6d8857 all the dispatchers started
2026-07-12 01:51:16.896 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 01:51:16.900 +09:00 [WRN] Failed to determine the https port for redirect.
2026-07-12 01:51:16.932 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:51:16.958 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:51:16.970 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 01:51:16.972 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 01:51:16.977 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:51:16.978 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:51:17.024 +09:00 [INF] Executed page /Account/Login in 63.0425ms
2026-07-12 01:51:17.025 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:51:17.026 +09:00 [INF] HTTP GET /Account/Login responded 200 in 126.7878 ms
2026-07-12 01:51:17.028 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 132.7688ms
2026-07-12 01:51:42.662 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 01:51:42.685 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:51:42.685 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:51:42.687 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 01:51:42.687 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 01:51:42.687 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:51:42.687 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:51:42.694 +09:00 [INF] Executed page /Account/Login in 9.2179ms
2026-07-12 01:51:42.695 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:51:42.695 +09:00 [INF] HTTP GET /Account/Login responded 200 in 32.3239 ms
2026-07-12 01:51:42.695 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 34.0934ms
2026-07-12 01:51:43.344 +09:00 [INF] Request starting HTTP/1.1 POST http://localhost:5265/Account/Login - application/x-www-form-urlencoded 218
2026-07-12 01:51:43.352 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:51:43.353 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:51:43.402 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync - ModelState is "Valid"
2026-07-12 01:51:44.200 +09:00 [INF] AuthenticationScheme: AdminCookie signed in.
2026-07-12 01:51:44.201 +09:00 [INF] [Login] User 'admin' authenticated successfully from ::1
2026-07-12 01:51:44.205 +09:00 [INF] Executed handler method OnPostAsync, returned result Microsoft.AspNetCore.Mvc.LocalRedirectResult.
2026-07-12 01:51:44.210 +09:00 [INF] Executing LocalRedirectResult, redirecting to /Admin/Dashboard.
2026-07-12 01:51:44.213 +09:00 [INF] Executed page /Account/Login in 859.92ms
2026-07-12 01:51:44.214 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:51:44.216 +09:00 [INF] HTTP POST /Account/Login responded 302 in 871.2169 ms
2026-07-12 01:51:44.218 +09:00 [INF] Request finished HTTP/1.1 POST http://localhost:5265/Account/Login - 302 0 null 873.6838ms
2026-07-12 01:51:44.222 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-12 01:51:44.232 +09:00 [INF] Executing endpoint '/Admin/Dashboard/Index'
2026-07-12 01:51:44.252 +09:00 [INF] Route matched with {page = "/Admin/Dashboard/Index"}. Executing page /Admin/Dashboard/Index
2026-07-12 01:51:44.253 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Dashboard.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:51:45.151 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:51:45.151 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:51:45.152 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:51:45.229 +09:00 [INF] Executed page /Admin/Dashboard/Index in 976.4261ms
2026-07-12 01:51:45.229 +09:00 [INF] Executed endpoint '/Admin/Dashboard/Index'
2026-07-12 01:51:45.230 +09:00 [INF] HTTP GET /Admin/Dashboard responded 200 in 1007.5596 ms
2026-07-12 01:51:45.230 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 200 null text/html; charset=utf-8 1008.1707ms
2026-07-12 01:51:45.238 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/css/admin.css - null null
2026-07-12 01:51:45.255 +09:00 [INF] Sending file. Request path: '/css/admin.css'. Physical path: 'C:\Temp\data_feed\src\dotnet\QuantEngine.Web\wwwroot\css\admin.css'
2026-07-12 01:51:45.256 +09:00 [INF] HTTP GET /css/admin.css responded 200 in 17.9079 ms
2026-07-12 01:51:45.256 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/css/admin.css - 200 3956 text/css 18.4204ms
2026-07-12 01:51:45.933 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Operations - null null
2026-07-12 01:51:45.937 +09:00 [INF] Executing endpoint '/Admin/Operations/Index'
2026-07-12 01:51:45.951 +09:00 [INF] Route matched with {page = "/Admin/Operations/Index"}. Executing page /Admin/Operations/Index
2026-07-12 01:51:45.952 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Operations.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:51:48.823 +09:00 [INF] Operations data loaded from Hangfire (4 recurring jobs, 2 servers)
2026-07-12 01:51:48.824 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:51:48.824 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:51:48.824 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:51:48.846 +09:00 [INF] Executed page /Admin/Operations/Index in 2894.9938ms
2026-07-12 01:51:48.847 +09:00 [INF] Executed endpoint '/Admin/Operations/Index'
2026-07-12 01:51:48.847 +09:00 [INF] HTTP GET /Admin/Operations responded 200 in 2913.4775 ms
2026-07-12 01:51:48.847 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Operations - 200 null text/html; charset=utf-8 2914.1336ms
2026-07-12 01:51:49.732 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 01:51:49.732 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:51:49.732 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:51:49.732 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 01:51:49.733 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 01:51:49.733 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:51:49.733 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:51:49.735 +09:00 [INF] Executed page /Account/Login in 2.6158ms
2026-07-12 01:51:49.735 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:51:49.735 +09:00 [INF] HTTP GET /Account/Login responded 200 in 3.4623 ms
2026-07-12 01:51:49.735 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 3.8328ms
2026-07-12 01:51:50.371 +09:00 [INF] Request starting HTTP/1.1 POST http://localhost:5265/Account/Login - application/x-www-form-urlencoded 218
2026-07-12 01:51:50.371 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:51:50.371 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:51:50.373 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync - ModelState is "Valid"
2026-07-12 01:51:50.857 +09:00 [INF] AuthenticationScheme: AdminCookie signed in.
2026-07-12 01:51:50.857 +09:00 [INF] [Login] User 'admin' authenticated successfully from ::1
2026-07-12 01:51:50.857 +09:00 [INF] Executed handler method OnPostAsync, returned result Microsoft.AspNetCore.Mvc.LocalRedirectResult.
2026-07-12 01:51:50.858 +09:00 [INF] Executing LocalRedirectResult, redirecting to /Admin/Dashboard.
2026-07-12 01:51:50.858 +09:00 [INF] Executed page /Account/Login in 486.6264ms
2026-07-12 01:51:50.858 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:51:50.858 +09:00 [INF] HTTP POST /Account/Login responded 302 in 487.2066 ms
2026-07-12 01:51:50.858 +09:00 [INF] Request finished HTTP/1.1 POST http://localhost:5265/Account/Login - 302 0 null 487.636ms
2026-07-12 01:51:50.862 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-12 01:51:50.865 +09:00 [INF] Executing endpoint '/Admin/Dashboard/Index'
2026-07-12 01:51:50.865 +09:00 [INF] Route matched with {page = "/Admin/Dashboard/Index"}. Executing page /Admin/Dashboard/Index
2026-07-12 01:51:50.865 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Dashboard.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:51:51.755 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:51:51.755 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:51:51.755 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:51:51.757 +09:00 [INF] Executed page /Admin/Dashboard/Index in 891.4622ms
2026-07-12 01:51:51.757 +09:00 [INF] Executed endpoint '/Admin/Dashboard/Index'
2026-07-12 01:51:51.757 +09:00 [INF] HTTP GET /Admin/Dashboard responded 200 in 894.9356 ms
2026-07-12 01:51:51.757 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 200 null text/html; charset=utf-8 895.2421ms
2026-07-12 01:51:51.766 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/css/admin.css - null null
2026-07-12 01:51:51.779 +09:00 [INF] Sending file. Request path: '/css/admin.css'. Physical path: 'C:\Temp\data_feed\src\dotnet\QuantEngine.Web\wwwroot\css\admin.css'
2026-07-12 01:51:51.779 +09:00 [INF] HTTP GET /css/admin.css responded 200 in 12.9029 ms
2026-07-12 01:51:51.780 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/css/admin.css - 200 3956 text/css 13.3629ms
2026-07-12 01:52:45.383 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 01:52:45.383 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:52:45.383 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:52:45.383 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 01:52:45.383 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 01:52:45.383 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:52:45.383 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:52:45.383 +09:00 [INF] Executed page /Account/Login in 0.5097ms
2026-07-12 01:52:45.384 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:52:45.384 +09:00 [INF] HTTP GET /Account/Login responded 200 in 0.8798 ms
2026-07-12 01:52:45.384 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 1.0536ms
2026-07-12 01:52:46.018 +09:00 [INF] Request starting HTTP/1.1 POST http://localhost:5265/Account/Login - application/x-www-form-urlencoded 218
2026-07-12 01:52:46.018 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:52:46.018 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:52:46.019 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync - ModelState is "Valid"
2026-07-12 01:52:46.475 +09:00 [INF] AuthenticationScheme: AdminCookie signed in.
2026-07-12 01:52:46.475 +09:00 [INF] [Login] User 'admin' authenticated successfully from ::1
2026-07-12 01:52:46.475 +09:00 [INF] Executed handler method OnPostAsync, returned result Microsoft.AspNetCore.Mvc.LocalRedirectResult.
2026-07-12 01:52:46.475 +09:00 [INF] Executing LocalRedirectResult, redirecting to /Admin/Dashboard.
2026-07-12 01:52:46.475 +09:00 [INF] Executed page /Account/Login in 456.9281ms
2026-07-12 01:52:46.475 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:52:46.475 +09:00 [INF] HTTP POST /Account/Login responded 302 in 457.3632 ms
2026-07-12 01:52:46.475 +09:00 [INF] Request finished HTTP/1.1 POST http://localhost:5265/Account/Login - 302 0 null 457.5915ms
2026-07-12 01:52:46.478 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - null null
2026-07-12 01:52:46.479 +09:00 [INF] Executing endpoint '/Admin/Dashboard/Index'
2026-07-12 01:52:46.479 +09:00 [INF] Route matched with {page = "/Admin/Dashboard/Index"}. Executing page /Admin/Dashboard/Index
2026-07-12 01:52:46.479 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Dashboard.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:52:47.363 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:52:47.363 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:52:47.363 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:52:47.366 +09:00 [INF] Executed page /Admin/Dashboard/Index in 886.9837ms
2026-07-12 01:52:47.366 +09:00 [INF] Executed endpoint '/Admin/Dashboard/Index'
2026-07-12 01:52:47.366 +09:00 [INF] HTTP GET /Admin/Dashboard responded 200 in 888.0406 ms
2026-07-12 01:52:47.367 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Dashboard - 200 null text/html; charset=utf-8 888.4149ms
2026-07-12 01:52:47.378 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/css/admin.css - null null
2026-07-12 01:52:47.380 +09:00 [INF] Sending file. Request path: '/css/admin.css'. Physical path: 'C:\Temp\data_feed\src\dotnet\QuantEngine.Web\wwwroot\css\admin.css'
2026-07-12 01:52:47.380 +09:00 [INF] HTTP GET /css/admin.css responded 200 in 2.4015 ms
2026-07-12 01:52:47.380 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/css/admin.css - 200 3956 text/css 2.7681ms
2026-07-12 01:52:48.337 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Collection - null null
2026-07-12 01:52:48.338 +09:00 [INF] Executing endpoint '/Admin/Collection/Index'
2026-07-12 01:52:48.355 +09:00 [INF] Route matched with {page = "/Admin/Collection/Index"}. Executing page /Admin/Collection/Index
2026-07-12 01:52:48.355 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Collection.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:52:48.581 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:52:48.581 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:52:48.581 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:52:48.591 +09:00 [INF] Executed page /Admin/Collection/Index in 236.0372ms
2026-07-12 01:52:48.591 +09:00 [INF] Executed endpoint '/Admin/Collection/Index'
2026-07-12 01:52:48.591 +09:00 [INF] HTTP GET /Admin/Collection responded 200 in 254.0162 ms
2026-07-12 01:52:48.592 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Collection - 200 null text/html; charset=utf-8 254.527ms
2026-07-12 01:52:49.211 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Monitoring - null null
2026-07-12 01:52:49.211 +09:00 [INF] Executing endpoint '/Admin/Monitoring/Index'
2026-07-12 01:52:49.217 +09:00 [INF] Route matched with {page = "/Admin/Monitoring/Index"}. Executing page /Admin/Monitoring/Index
2026-07-12 01:52:49.218 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Monitoring.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:52:49.440 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:52:49.440 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:52:49.440 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:52:49.453 +09:00 [INF] Executed page /Admin/Monitoring/Index in 235.3992ms
2026-07-12 01:52:49.453 +09:00 [INF] Executed endpoint '/Admin/Monitoring/Index'
2026-07-12 01:52:49.453 +09:00 [INF] HTTP GET /Admin/Monitoring responded 200 in 242.4802 ms
2026-07-12 01:52:49.453 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Monitoring - 200 null text/html; charset=utf-8 242.724ms
2026-07-12 01:52:50.106 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Admin/Users - null null
2026-07-12 01:52:50.107 +09:00 [INF] Executing endpoint '/Admin/Users/Index'
2026-07-12 01:52:50.117 +09:00 [INF] Route matched with {page = "/Admin/Users/Index"}. Executing page /Admin/Users/Index
2026-07-12 01:52:50.118 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Admin.Users.IndexModel.OnGetAsync - ModelState is "Valid"
2026-07-12 01:52:50.339 +09:00 [INF] Executed handler method OnGetAsync, returned result .
2026-07-12 01:52:50.339 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:52:50.339 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:52:50.346 +09:00 [INF] Executed page /Admin/Users/Index in 228.3491ms
2026-07-12 01:52:50.346 +09:00 [INF] Executed endpoint '/Admin/Users/Index'
2026-07-12 01:52:50.346 +09:00 [INF] HTTP GET /Admin/Users responded 200 in 239.3317 ms
2026-07-12 01:52:50.346 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Admin/Users - 200 null text/html; charset=utf-8 239.5416ms
2026-07-12 01:56:32.591 +09:00 [INF] Registered 10 endpoints in 168 milliseconds.
2026-07-12 01:56:32.627 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-12 01:56:34.908 +09:00 [INF] ✅ Database migration completed successfully
2026-07-12 01:56:37.976 +09:00 [INF] Database migration and initialization successful
2026-07-12 01:56:38.000 +09:00 [INF] User profile is available. Using 'C:\Users\kjh20\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2026-07-12 01:56:38.043 +09:00 [INF] Start installing Hangfire SQL objects...
2026-07-12 01:56:42.666 +09:00 [INF] Hangfire SQL objects installed.
2026-07-12 01:56:42.681 +09:00 [INF] Initializing Hangfire schedules...
2026-07-12 01:56:50.823 +09:00 [INF] Hangfire schedules initialized successfully
2026-07-12 01:56:50.988 +09:00 [INF] Now listening on: http://localhost:5265
2026-07-12 01:56:50.992 +09:00 [INF] Starting Hangfire Server using job storage: 'PostgreSQL Server: Host: 127.0.0.1, DB: quantenginedb, Schema: hangfire'
2026-07-12 01:56:50.992 +09:00 [INF] Using the following options for PostgreSQL job storage:
2026-07-12 01:56:50.992 +09:00 [INF] Queue poll interval: 00:00:15.
2026-07-12 01:56:50.992 +09:00 [INF] Invisibility timeout: 00:30:00.
2026-07-12 01:56:50.992 +09:00 [INF] Use sliding invisibility timeout: False.
2026-07-12 01:56:50.992 +09:00 [INF] Using the following options for Hangfire Server:
Worker count: 32
Listening queues: 'default'
Shutdown timeout: 00:00:15
Schedule polling interval: 00:00:15
2026-07-12 01:56:51.000 +09:00 [INF] Application started. Press Ctrl+C to shut down.
2026-07-12 01:56:51.000 +09:00 [INF] Hosting environment: Development
2026-07-12 01:56:51.000 +09:00 [INF] Content root path: C:\Temp\data_feed\src\dotnet\QuantEngine.Web
2026-07-12 01:56:51.234 +09:00 [INF] Server kimjaehyun-note:18688:50858618 successfully announced in 231.3723 ms
2026-07-12 01:56:51.239 +09:00 [INF] Server kimjaehyun-note:18688:50858618 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2026-07-12 01:56:51.251 +09:00 [INF] Server kimjaehyun-note:18688:50858618 all the dispatchers started
2026-07-12 01:56:52.918 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 01:56:52.923 +09:00 [WRN] Failed to determine the https port for redirect.
2026-07-12 01:56:52.975 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 01:56:53.000 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 01:56:53.008 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 01:56:53.009 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 01:56:53.011 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 01:56:53.012 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 01:56:53.043 +09:00 [INF] Executed page /Account/Login in 38.7733ms
2026-07-12 01:56:53.043 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 01:56:53.045 +09:00 [INF] HTTP GET /Account/Login responded 200 in 123.2950 ms
2026-07-12 01:56:53.048 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 131.2244ms
2026-07-12 10:29:03.142 +09:00 [INF] Registered 10 endpoints in 4,906 milliseconds.
2026-07-12 10:29:03.556 +09:00 [INF] 🔄 Starting database migration with DbUp...
2026-07-12 10:29:07.251 +09:00 [ERR] ❌ Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-12 10:29:07.251 +09:00 [ERR] ❌ Database migration failed
System.InvalidOperationException: Database migration failed: 28P01: password authentication failed for user "quantengine_app"
at QuantEngine.Infrastructure.Data.DbMigrator.Migrate() in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Data\DbMigrator.cs:line 39
2026-07-12 10:29:07.276 +09:00 [WRN] Database initialization warning (development only): Database migration failed: 28P01: password authentication failed for user "quantengine_app"
2026-07-12 10:29:07.442 +09:00 [INF] Initializing Hangfire schedules...
2026-07-12 10:29:07.565 +09:00 [INF] Hangfire schedules initialized successfully
2026-07-12 10:29:07.677 +09:00 [INF] Creating key {49191b44-5683-456a-b1ee-0ddbc2c7fc29} with creation date 2026-07-12 01:29:07Z, activation date 2026-07-12 01:29:07Z, and expiration date 2026-10-10 01:29:07Z.
2026-07-12 10:29:07.689 +09:00 [WRN] No XML encryptor configured. Key {49191b44-5683-456a-b1ee-0ddbc2c7fc29} may be persisted to storage in unencrypted form.
2026-07-12 10:29:07.697 +09:00 [INF] Writing data to file 'C:\Users\kjh20\AppData\Local\quantengine-keys\key-49191b44-5683-456a-b1ee-0ddbc2c7fc29.xml'.
2026-07-12 10:29:07.829 +09:00 [INF] Now listening on: http://localhost:5265
2026-07-12 10:29:07.833 +09:00 [INF] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage'
2026-07-12 10:29:07.834 +09:00 [INF] Using the following options for Hangfire Server:
Worker count: 32
Listening queues: 'default'
Shutdown timeout: 00:00:15
Schedule polling interval: 00:00:15
2026-07-12 10:29:07.843 +09:00 [INF] Application started. Press Ctrl+C to shut down.
2026-07-12 10:29:07.843 +09:00 [INF] Hosting environment: Development
2026-07-12 10:29:07.843 +09:00 [INF] Content root path: C:\Temp\data_feed\src\dotnet\QuantEngine.Web
2026-07-12 10:29:07.850 +09:00 [INF] Server kimjaehyun-note:27604:c5019131 successfully announced in 5.0644 ms
2026-07-12 10:29:07.852 +09:00 [INF] Server kimjaehyun-note:27604:c5019131 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2026-07-12 10:29:07.863 +09:00 [INF] Server kimjaehyun-note:27604:c5019131 all the dispatchers started
2026-07-12 10:29:10.923 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 10:29:10.929 +09:00 [WRN] Failed to determine the https port for redirect.
2026-07-12 10:29:10.969 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 10:29:10.987 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 10:29:10.994 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 10:29:10.995 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 10:29:10.997 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 10:29:10.998 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 10:29:11.026 +09:00 [INF] Executed page /Account/Login in 35.8464ms
2026-07-12 10:29:11.027 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 10:29:11.028 +09:00 [INF] HTTP GET /Account/Login responded 200 in 99.7375 ms
2026-07-12 10:29:11.030 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 121.6214ms
2026-07-12 10:54:40.138 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-12 10:54:40.144 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-12 10:54:40.145 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-12 10:54:40.145 +09:00 [INF] HTTP GET /login responded 302 in 6.0857 ms
2026-07-12 10:54:40.146 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 9.206ms
2026-07-12 10:54:40.153 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 10:54:40.163 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 10:54:40.163 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 10:54:40.165 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 10:54:40.165 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 10:54:40.165 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 10:54:40.166 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 10:54:40.179 +09:00 [INF] Executed page /Account/Login in 15.2716ms
2026-07-12 10:54:40.179 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 10:54:40.179 +09:00 [INF] HTTP GET /Account/Login responded 200 in 25.4427 ms
2026-07-12 10:54:40.180 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 26.1015ms
2026-07-12 10:54:40.185 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-12 10:54:40.186 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-12 10:54:40.187 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-12 10:54:40.187 +09:00 [INF] HTTP GET /login responded 302 in 1.4987 ms
2026-07-12 10:54:40.187 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 2.2724ms
2026-07-12 10:54:40.190 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 10:54:40.191 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 10:54:40.191 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 10:54:40.195 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 10:54:40.196 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 10:54:40.196 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 10:54:40.196 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 10:54:40.203 +09:00 [INF] Executed page /Account/Login in 11.1274ms
2026-07-12 10:54:40.203 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 10:54:40.203 +09:00 [INF] HTTP GET /Account/Login responded 200 in 12.4260 ms
2026-07-12 10:54:40.203 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 13.0096ms
2026-07-12 10:54:44.086 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 10:54:44.087 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 10:54:44.087 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 10:54:44.087 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 10:54:44.087 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 10:54:44.088 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 10:54:44.088 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 10:54:44.088 +09:00 [INF] Executed page /Account/Login in 1.1538ms
2026-07-12 10:54:44.089 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 10:54:44.089 +09:00 [INF] HTTP GET /Account/Login responded 200 in 1.8741 ms
2026-07-12 10:54:44.089 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 2.4044ms
2026-07-12 10:54:44.647 +09:00 [INF] Request starting HTTP/1.1 POST http://localhost:5265/Account/Login - application/x-www-form-urlencoded 212
2026-07-12 10:54:44.649 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 10:54:44.649 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 10:54:44.686 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync - ModelState is "Valid"
2026-07-12 10:54:48.980 +09:00 [INF] Executed page /Account/Login in 4330.4506ms
2026-07-12 10:54:48.981 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 10:54:48.984 +09:00 [ERR] HTTP POST /Account/Login responded 500 in 4336.7156 ms
Npgsql.PostgresException (0x80004005): 28P01: password authentication failed for user "quantengine_app"
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List`1 mechanisms, String username, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Authenticate(String username, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.<Get>g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnection.<Open>g__OpenAsync|42_0(Boolean async, CancellationToken cancellationToken)
at Dapper.SqlMapper.QueryRowAsync[T](IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in /_/Dapper/SqlMapper.Async.cs:line 488
at QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(String username) in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Repositories\WorkspaceRepository.cs:line 35
at QuantEngine.Web.Services.AuthService.AuthenticateAsync(String username, String password, String ipAddress) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Services\AuthService.cs:line 26
at QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(String username, String password, Boolean rememberUsername) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Pages\Account\Login.cshtml.cs:line 49
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
Exception data:
Severity: FATAL
SqlState: 28P01
MessageText: password authentication failed for user "quantengine_app"
File: auth.c
Line: 317
Routine: auth_failed
2026-07-12 10:54:49.034 +09:00 [ERR] An unhandled exception has occurred while executing the request.
Npgsql.PostgresException (0x80004005): 28P01: password authentication failed for user "quantengine_app"
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List`1 mechanisms, String username, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Authenticate(String username, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.<Get>g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnection.<Open>g__OpenAsync|42_0(Boolean async, CancellationToken cancellationToken)
at Dapper.SqlMapper.QueryRowAsync[T](IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in /_/Dapper/SqlMapper.Async.cs:line 488
at QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(String username) in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Repositories\WorkspaceRepository.cs:line 35
at QuantEngine.Web.Services.AuthService.AuthenticateAsync(String username, String password, String ipAddress) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Services\AuthService.cs:line 26
at QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(String username, String password, Boolean rememberUsername) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Pages\Account\Login.cshtml.cs:line 49
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Exception data:
Severity: FATAL
SqlState: 28P01
MessageText: password authentication failed for user "quantengine_app"
File: auth.c
Line: 317
Routine: auth_failed
2026-07-12 10:54:49.111 +09:00 [INF] Request finished HTTP/1.1 POST http://localhost:5265/Account/Login - 500 null text/html; charset=utf-8 4464.5985ms
2026-07-12 10:54:49.205 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/api/collection/runs?limit=20 - null null
2026-07-12 10:54:49.207 +09:00 [INF] Executing endpoint 'HTTP: GET /api/collection/runs'
2026-07-12 10:54:52.982 +09:00 [INF] Executed endpoint 'HTTP: GET /api/collection/runs'
2026-07-12 10:54:52.982 +09:00 [ERR] HTTP GET /api/collection/runs responded 500 in 3777.5166 ms
2026-07-12 10:54:52.983 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/api/collection/runs?limit=20 - 500 null application/problem+json; charset=utf-8 3778.0479ms
2026-07-12 13:02:07.505 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-12 13:02:07.523 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-12 13:02:07.523 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-12 13:02:07.524 +09:00 [INF] HTTP GET /login responded 302 in 16.0250 ms
2026-07-12 13:02:07.529 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 21.1915ms
2026-07-12 13:02:07.531 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 13:02:07.531 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 13:02:07.540 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 13:02:07.546 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 13:02:07.548 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 13:02:07.548 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 13:02:07.550 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 13:02:07.581 +09:00 [INF] Executed page /Account/Login in 40.445ms
2026-07-12 13:02:07.581 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 13:02:07.581 +09:00 [INF] HTTP GET /Account/Login responded 200 in 49.9978 ms
2026-07-12 13:02:07.581 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 50.4194ms
2026-07-12 13:02:07.585 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/login - null null
2026-07-12 13:02:07.585 +09:00 [INF] Executing endpoint 'HTTP: GET /login'
2026-07-12 13:02:07.585 +09:00 [INF] Executed endpoint 'HTTP: GET /login'
2026-07-12 13:02:07.585 +09:00 [INF] HTTP GET /login responded 302 in 0.3455 ms
2026-07-12 13:02:07.585 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/login - 302 0 null 0.6015ms
2026-07-12 13:02:07.586 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 13:02:07.587 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 13:02:07.587 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 13:02:07.587 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 13:02:07.587 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 13:02:07.588 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 13:02:07.588 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 13:02:07.589 +09:00 [INF] Executed page /Account/Login in 1.3984ms
2026-07-12 13:02:07.589 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 13:02:07.589 +09:00 [INF] HTTP GET /Account/Login responded 200 in 2.2170 ms
2026-07-12 13:02:07.589 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 2.4854ms
2026-07-12 13:02:11.141 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/Account/Login - null null
2026-07-12 13:02:11.142 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 13:02:11.142 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 13:02:11.142 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnGet - ModelState is "Valid"
2026-07-12 13:02:11.143 +09:00 [INF] Executed handler method OnGet, returned result .
2026-07-12 13:02:11.143 +09:00 [INF] Executing an implicit handler method - ModelState is "Valid"
2026-07-12 13:02:11.143 +09:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
2026-07-12 13:02:11.144 +09:00 [INF] Executed page /Account/Login in 1.7154ms
2026-07-12 13:02:11.144 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 13:02:11.144 +09:00 [INF] HTTP GET /Account/Login responded 200 in 2.8689 ms
2026-07-12 13:02:11.144 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/Account/Login - 200 null text/html; charset=utf-8 3.2765ms
2026-07-12 13:02:11.640 +09:00 [INF] Request starting HTTP/1.1 POST http://localhost:5265/Account/Login - application/x-www-form-urlencoded 212
2026-07-12 13:02:11.643 +09:00 [INF] Executing endpoint '/Account/Login'
2026-07-12 13:02:11.644 +09:00 [INF] Route matched with {page = "/Account/Login"}. Executing page /Account/Login
2026-07-12 13:02:11.684 +09:00 [INF] Executing handler method QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync - ModelState is "Valid"
2026-07-12 13:02:15.491 +09:00 [INF] Executed page /Account/Login in 3846.9073ms
2026-07-12 13:02:15.493 +09:00 [INF] Executed endpoint '/Account/Login'
2026-07-12 13:02:15.495 +09:00 [ERR] HTTP POST /Account/Login responded 500 in 3854.1437 ms
Npgsql.PostgresException (0x80004005): 28P01: password authentication failed for user "quantengine_app"
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List`1 mechanisms, String username, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Authenticate(String username, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.<Get>g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnection.<Open>g__OpenAsync|42_0(Boolean async, CancellationToken cancellationToken)
at Dapper.SqlMapper.QueryRowAsync[T](IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in /_/Dapper/SqlMapper.Async.cs:line 488
at QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(String username) in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Repositories\WorkspaceRepository.cs:line 35
at QuantEngine.Web.Services.AuthService.AuthenticateAsync(String username, String password, String ipAddress) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Services\AuthService.cs:line 26
at QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(String username, String password, Boolean rememberUsername) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Pages\Account\Login.cshtml.cs:line 49
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
Exception data:
Severity: FATAL
SqlState: 28P01
MessageText: password authentication failed for user "quantengine_app"
File: auth.c
Line: 317
Routine: auth_failed
2026-07-12 13:02:15.530 +09:00 [ERR] An unhandled exception has occurred while executing the request.
Npgsql.PostgresException (0x80004005): 28P01: password authentication failed for user "quantengine_app"
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List`1 mechanisms, String username, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Authenticate(String username, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, String username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.Internal.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.PoolingDataSource.<Get>g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnection.<Open>g__OpenAsync|42_0(Boolean async, CancellationToken cancellationToken)
at Dapper.SqlMapper.QueryRowAsync[T](IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in /_/Dapper/SqlMapper.Async.cs:line 488
at QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(String username) in C:\Temp\data_feed\src\dotnet\QuantEngine.Infrastructure\Repositories\WorkspaceRepository.cs:line 35
at QuantEngine.Web.Services.AuthService.AuthenticateAsync(String username, String password, String ipAddress) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Services\AuthService.cs:line 26
at QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(String username, String password, Boolean rememberUsername) in C:\Temp\data_feed\src\dotnet\QuantEngine.Web\Pages\Account\Login.cshtml.cs:line 49
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Exception data:
Severity: FATAL
SqlState: 28P01
MessageText: password authentication failed for user "quantengine_app"
File: auth.c
Line: 317
Routine: auth_failed
2026-07-12 13:02:15.576 +09:00 [INF] Request finished HTTP/1.1 POST http://localhost:5265/Account/Login - 500 null text/html; charset=utf-8 3935.4857ms
2026-07-12 13:02:15.664 +09:00 [INF] Request starting HTTP/1.1 GET http://localhost:5265/api/collection/runs?limit=20 - null null
2026-07-12 13:02:15.668 +09:00 [INF] Executing endpoint 'HTTP: GET /api/collection/runs'
2026-07-12 13:02:19.317 +09:00 [INF] Executed endpoint 'HTTP: GET /api/collection/runs'
2026-07-12 13:02:19.318 +09:00 [ERR] HTTP GET /api/collection/runs responded 500 in 3653.6595 ms
2026-07-12 13:02:19.318 +09:00 [INF] Request finished HTTP/1.1 GET http://localhost:5265/api/collection/runs?limit=20 - 500 null application/problem+json; charset=utf-8 3654.0261ms
Binary file not shown.
+4 -2
View File
@@ -1,4 +1,6 @@
{
"status": "passed",
"failedTests": []
"status": "failed",
"failedTests": [
"90c6053e24d905f92d61-fe6c7d0382f9666a596d"
]
}
@@ -0,0 +1,259 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: evidence\qe-m1-02-collection-run.spec.ts >> QE-M1-02: Collection Run List & Detail Verification >> QE-M1-02: Collection run renders in list with API-derived expected values
- Location: tests\e2e\evidence\qe-m1-02-collection-run.spec.ts:24:3
# Error details
```
Error: expect(received).toBeTruthy()
Received: false
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- heading "An unhandled exception occurred while processing the request." [level=1] [ref=e2]
- generic [ref=e3]: "PostgresException: 28P01: password authentication failed for user \"quantengine_app\""
- paragraph [ref=e4]: Npgsql.Internal.NpgsqlConnector.ReadMessageLong(bool async, DataRowLoadingMode dataRowLoadingMode, bool readingNotifications, bool isReadingPrependedMessage)
- list [ref=e5]:
- listitem [ref=e6] [cursor=pointer]: Stack
- listitem [ref=e7] [cursor=pointer]: Query
- listitem [ref=e8] [cursor=pointer]: Cookies
- listitem [ref=e9] [cursor=pointer]: Headers
- listitem [ref=e10] [cursor=pointer]: Routing
- list [ref=e12]:
- listitem [ref=e13]:
- 'heading "PostgresException: 28P01: password authentication failed for user \"quantengine_app\"" [level=2] [ref=e14]'
- list [ref=e15]:
- listitem [ref=e16]:
- heading "Npgsql.Internal.NpgsqlConnector.ReadMessageLong(bool async, DataRowLoadingMode dataRowLoadingMode, bool readingNotifications, bool isReadingPrependedMessage)" [level=3] [ref=e17]
- listitem [ref=e18]:
- heading "System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder<TResult>+StateMachineBox<TStateMachine>.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(short token)" [level=3] [ref=e19]
- listitem [ref=e20]:
- heading "Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List<string> mechanisms, string username, bool async, CancellationToken cancellationToken)" [level=3] [ref=e21]
- listitem [ref=e22]:
- heading "Npgsql.Internal.NpgsqlConnector.Authenticate(string username, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e23]
- listitem [ref=e24]:
- heading "Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, string username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e25]
- listitem [ref=e26]:
- heading "Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, string username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e27]
- listitem [ref=e28]:
- heading "Npgsql.Internal.NpgsqlConnector.<Open>g__OpenCore|209_0(NpgsqlConnector conn, string username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e29]
- listitem [ref=e30]:
- heading "Npgsql.Internal.NpgsqlConnector.Open(NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e31]
- listitem [ref=e32]:
- heading "Npgsql.PoolingDataSource.OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e33]
- listitem [ref=e34]:
- heading "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()" [level=3] [ref=e35]
- listitem [ref=e36]:
- heading "Npgsql.PoolingDataSource.<Get>g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e37]
- listitem [ref=e38]:
- heading "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()" [level=3] [ref=e39]
- listitem [ref=e40]:
- heading "Npgsql.NpgsqlConnection.<Open>g__OpenAsync|42_0(bool async, CancellationToken cancellationToken)" [level=3] [ref=e41]
- listitem [ref=e42]:
- heading "Dapper.SqlMapper.QueryRowAsync<T>(IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in SqlMapper.Async.cs" [level=3] [ref=e43]:
- text: Dapper.SqlMapper.QueryRowAsync<T>(IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in
- code [ref=e44]: SqlMapper.Async.cs
- listitem [ref=e45]:
- heading "QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(string username) in WorkspaceRepository.cs" [level=3] [ref=e46]:
- text: QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(string username) in
- code [ref=e47]: WorkspaceRepository.cs
- button "+" [ref=e48] [cursor=pointer]
- list [ref=e50]:
- listitem [ref=e51]: return await conn.QueryFirstOrDefaultAsync<WorkspaceAccount>(@"
- listitem [ref=e52]:
- heading "QuantEngine.Web.Services.AuthService.AuthenticateAsync(string username, string password, string ipAddress) in AuthService.cs" [level=3] [ref=e53]:
- text: QuantEngine.Web.Services.AuthService.AuthenticateAsync(string username, string password, string ipAddress) in
- code [ref=e54]: AuthService.cs
- button "+" [ref=e55] [cursor=pointer]
- list [ref=e57]:
- listitem [ref=e58]: var account = await _workspaceRepository.GetAccountByUsernameAsync(username.Trim());
- listitem [ref=e59]:
- heading "QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(string username, string password, bool rememberUsername) in Login.cshtml.cs" [level=3] [ref=e60]:
- text: QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(string username, string password, bool rememberUsername) in
- code [ref=e61]: Login.cshtml.cs
- button "+" [ref=e62] [cursor=pointer]
- list [ref=e64]:
- listitem [ref=e65]: var account = await _authService.AuthenticateAsync(username, password, ipAddress);
- listitem [ref=e66]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Convert<T>(object taskAsObject)" [level=3] [ref=e67]
- listitem [ref=e68]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Execute(object receiver, object[] arguments)" [level=3] [ref=e69]
- listitem [ref=e70]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()" [level=3] [ref=e71]
- listitem [ref=e72]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()" [level=3] [ref=e73]
- listitem [ref=e74]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)" [level=3] [ref=e75]
- listitem [ref=e76]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)" [level=3] [ref=e77]
- listitem [ref=e78]:
- heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()" [level=3] [ref=e79]
- listitem [ref=e80]:
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)" [level=3] [ref=e81]
- listitem [ref=e82]:
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)" [level=3] [ref=e83]
- listitem [ref=e84]:
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)" [level=3] [ref=e85]
- listitem [ref=e86]:
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)" [level=3] [ref=e87]
- listitem [ref=e88]:
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)" [level=3] [ref=e89]
- listitem [ref=e90]:
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)" [level=3] [ref=e91]
- listitem [ref=e92]:
- heading "Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)" [level=3] [ref=e93]
- listitem [ref=e94]:
- heading "Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)" [level=3] [ref=e95]
- listitem [ref=e96]:
- heading "Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)" [level=3] [ref=e97]
- listitem [ref=e98]:
- heading "Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)" [level=3] [ref=e99]
- listitem [ref=e100]:
- heading "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)" [level=3] [ref=e101]
- listitem [ref=e102]:
- button "Show raw exception details" [ref=e104] [cursor=pointer]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 |
5 | test.describe('QE-M1-02: Collection Run List & Detail Verification', () => {
6 | // Login before each test
7 | test.beforeEach(async ({ page }) => {
8 | await page.goto('/Account/Login');
9 | await page.waitForLoadState('domcontentloaded');
10 |
11 | // Fill login form with credentials (admin/admin)
12 | const usernameInput = page.locator('#username');
13 | const passwordInput = page.locator('#password');
14 | const loginButton = page.locator('#loginBtn');
15 |
16 | await usernameInput.fill('admin');
17 | await passwordInput.fill('admin');
18 | await loginButton.click();
19 |
20 | // Wait for login to complete
21 | await page.waitForLoadState('domcontentloaded');
22 | });
23 |
24 | test('QE-M1-02: Collection run renders in list with API-derived expected values', async ({ page }) => {
25 | // Step 1: Fetch expected values from API (source of truth)
26 | const apiResponse = await page.request.get('/api/collection/runs?limit=20');
> 27 | expect(apiResponse.ok()).toBeTruthy();
| ^ Error: expect(received).toBeTruthy()
28 |
29 | const responseJson = await apiResponse.json();
30 | const runs = (responseJson as any).runs || [];
31 |
32 | // Fail if no collection runs exist in database
33 | if (runs.length === 0) {
34 | throw new Error(
35 | 'No collection runs in DB — run the daily-collection job first. ' +
36 | 'Expected at least 1 run from kis_collection_runs table.'
37 | );
38 | }
39 |
40 | // Extract expected values from most recent run (first in list)
41 | const expectedRun = runs[0];
42 | const expectedRunId = expectedRun.runId;
43 | const expectedTotalSnapshots = expectedRun.totalSnapshots ?? 0;
44 | const expectedStatus = expectedRun.status; // e.g., "completed", "running", "failed"
45 |
46 | // Map status to Korean text (same logic as Index.cshtml — unknown statuses
47 | // like COMPLETED_WITH_ERRORS render the raw status string in a secondary badge)
48 | let expectedStatusText = String(expectedStatus ?? '');
49 | if (expectedStatus?.toLowerCase() === 'completed') {
50 | expectedStatusText = '완료';
51 | } else if (expectedStatus?.toLowerCase() === 'running') {
52 | expectedStatusText = '진행 중';
53 | } else if (expectedStatus?.toLowerCase() === 'failed') {
54 | expectedStatusText = '실패';
55 | }
56 |
57 | console.log(
58 | `\n=== QE-M1-02 Test Started ===\n` +
59 | `Expected RunId: ${expectedRunId}\n` +
60 | `Expected TotalSnapshots: ${expectedTotalSnapshots}\n` +
61 | `Expected Status: ${expectedStatus} (rendered as: ${expectedStatusText})\n`
62 | );
63 |
64 | // Step 2: Navigate to Collection admin page
65 | await page.goto('/Admin/Collection');
66 | await page.waitForLoadState('domcontentloaded');
67 |
68 | // Step 3: Verify page title contains "데이터 수집" (collection)
69 | const pageTitle = await page.title();
70 | expect(pageTitle).toContain('데이터 수집');
71 |
72 | // Step 4: Assert that a row containing the expected runId is visible
73 | const runIdCell = page.locator(`td:has-text("${expectedRunId}")`);
74 | await expect(runIdCell).toBeVisible();
75 | console.log(`✓ RunId row found and visible: ${expectedRunId}`);
76 |
77 | // Step 5: Find the row containing this runId and verify the snapshot count
78 | const tableRow = runIdCell.locator('xpath=ancestor::tr');
79 |
80 | // Within the row, find all td elements and map to columns
81 | // Columns: 실행 ID (0), 시작 시간 (1), 종료 시간 (2), 상태 (3), 스냅샷 수 (4), 오류 수 (5)
82 | const cells = tableRow.locator('td');
83 | const cellCount = await cells.count();
84 | expect(cellCount).toBeGreaterThanOrEqual(5); // At least 5 columns
85 |
86 | // Cell 4 (index 4) is "스냅샷 수" (total snapshots)
87 | const snapshotCell = cells.nth(4);
88 | const snapshotText = await snapshotCell.textContent();
89 | expect(snapshotText?.trim()).toBe(String(expectedTotalSnapshots));
90 | console.log(`✓ Snapshot count matches: ${snapshotText?.trim()} == ${expectedTotalSnapshots}`);
91 |
92 | // Cell 3 (index 3) is "상태" (status badge)
93 | const statusCell = cells.nth(3);
94 | const statusBadge = statusCell.locator('span.badge');
95 | const statusBadgeText = await statusBadge.textContent();
96 | expect(statusBadgeText?.trim()).toBe(expectedStatusText);
97 | console.log(`✓ Status badge matches: ${statusBadgeText?.trim()} == ${expectedStatusText}`);
98 |
99 | // Step 6: Create screenshot directory and take screenshot of collection list
100 | const screenshotDir = path.join(process.cwd(), 'Temp', 'evidence', 'QE-M1-02', 'screenshots');
101 | fs.mkdirSync(screenshotDir, { recursive: true });
102 |
103 | await page.screenshot({
104 | path: path.join(screenshotDir, '01-collection-page.png'),
105 | fullPage: true,
106 | });
107 | console.log(`✓ Screenshot saved: 01-collection-page.png`);
108 |
109 | // Step 7: Navigate to the run detail page
110 | // The detail page route is /Admin/Collection/{runId}
111 | await page.goto(`/Admin/Collection/${expectedRunId}`);
112 | await page.waitForLoadState('domcontentloaded');
113 |
114 | // Step 8: Verify detail page title contains the runId
115 | const detailPageTitle = await page.title();
116 | expect(detailPageTitle).toContain('수집 실행 상세');
117 |
118 | // Step 9: Verify that the RunId is displayed on the detail page
119 | // The page title shows: "수집 실행 상세 - {runId}"
120 | const pageHeading = page.locator('h2.page-title');
121 | const headingText = await pageHeading.textContent();
122 | expect(headingText).toContain(expectedRunId);
123 | console.log(`✓ Detail page title contains RunId: ${headingText}`);
124 |
125 | // Step 10: Verify snapshots count is displayed on detail page
126 | // The snapshot count appears in a card with "스냅샷 수" as the title
127 | const snapshotCountCard = page.locator('h4.card-title:has-text("스냅샷 수")');
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB