QuantEngine MudBlazor UI: Complete Phase 1-8 Implementation #14

Merged
kjh2064 merged 12 commits from feature/smartadmin-bootstrap-migration into main 2026-07-05 17:11:45 +09:00
4 changed files with 542 additions and 79 deletions
Showing only changes of commit 736addef70 - Show all commits
@@ -1,3 +1,98 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject MudThemeProvider MudThemeProvider
<MudThemeProvider @ref="mudThemeProvider" @bind-IsDarkMode="@isDarkMode" />
<div class="auth-container">
<!-- Left Panel - Branding -->
<MudHidden Breakpoint="Breakpoint.SmAndDown" Invert="true" Class="auth-left-panel">
<div class="auth-branding">
<div class="auth-logo">
<MudIcon Icon="@Icons.Material.Filled.AnalyticsFill" Size="Size.Large" />
</div>
<MudText Typo="Typo.h3" Class="auth-title">
QuantEngine
</MudText>
<MudText Typo="Typo.body1" Class="auth-subtitle">
퇴직 자산 포트폴리오 관리 시스템
</MudText>
<div class="auth-features mt-8">
<div class="auth-feature">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" />
<MudText Typo="Typo.body2">실시간 자산 모니터링</MudText>
</div>
<div class="auth-feature">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" />
<MudText Typo="Typo.body2">AI 기반 분석</MudText>
</div>
<div class="auth-feature">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" />
<MudText Typo="Typo.body2">종합 보고서</MudText>
</div>
</div>
</div>
<!-- Dark Mode Toggle -->
<div class="auth-theme-toggle">
<MudIconButton Icon="@(isDarkMode ? Icons.Material.Filled.Brightness4 : Icons.Material.Filled.Brightness7)"
Color="Color.Inherit"
OnClick="ToggleDarkMode"
Title="@(isDarkMode ? "라이트 모드" : "다크 모드")"
Size="Size.Large" />
</div>
</MudHidden>
<!-- Right Panel - Auth Content -->
<div class="auth-right-panel">
<!-- Mobile Header -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<div class="auth-mobile-header">
<MudText Typo="Typo.h5" Class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.AnalyticsFill" Size="Size.Medium" Class="mr-2" />
QuantEngine
</MudText>
<MudIconButton Icon="@(isDarkMode ? Icons.Material.Filled.Brightness4 : Icons.Material.Filled.Brightness7)"
Color="Color.Inherit"
OnClick="ToggleDarkMode"
Title="@(isDarkMode ? "라이트 모드" : "다크 모드")" />
</div>
</MudHidden>
<!-- Content -->
<div class="auth-content">
@Body @Body
</div>
<!-- Footer -->
<div class="auth-footer">
<MudText Typo="Typo.caption" Class="auth-footer-text">
© 2026 QuantEngine. 모든 권리 예약.
</MudText>
<div class="auth-footer-links">
<MudLink Href="/" Typo="Typo.caption">서비스 약관</MudLink>
<MudText Typo="Typo.caption">·</MudText>
<MudLink Href="/" Typo="Typo.caption">개인정보 처리방침</MudLink>
</div>
</div>
</div>
</div>
@code {
private MudThemeProvider mudThemeProvider = default!;
private bool isDarkMode = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isDarkMode = await mudThemeProvider.GetDarkModeAsync() ?? false;
StateHasChanged();
}
}
private async Task ToggleDarkMode()
{
isDarkMode = !isDarkMode;
await mudThemeProvider.SetDarkModeAsync(isDarkMode);
}
}
@@ -0,0 +1,260 @@
/* 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;
}
}
@@ -2,32 +2,98 @@
@inject HttpClient Http @inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider @inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject MudThemeProvider MudThemeProvider
<MudLayout> <MudLayout>
<MudAppBar Elevation="1" Dense="true"> <!-- 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)" /> <MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@(() => navOpen = !navOpen)" />
<MudText Typo="Typo.h6">QuantEngine v@appVersion</MudText> </MudHidden>
<MudText Typo="Typo.h6" Class="ml-2">
<MudIcon Icon="@Icons.Material.Filled.Dashboard" Class="me-2" />
QuantEngine
</MudText>
<MudSpacer /> <MudSpacer />
<!-- Theme Toggle -->
<MudIconButton Icon="@(isDarkMode ? Icons.Material.Filled.Brightness4 : Icons.Material.Filled.Brightness7)"
Color="Color.Inherit"
OnClick="ToggleDarkMode"
Title="@(isDarkMode ? "라이트 모드" : "다크 모드")" />
<!-- User Menu -->
<AuthorizeView> <AuthorizeView>
<Authorized> <Authorized>
<MudText Typo="Typo.body2">관리자 (@context.User.Identity?.Name)</MudText> <MudMenu AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopRight" Class="ml-2">
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="HandleLogoutAsync">로그아웃</MudButton> <ActivatorContent>
<MudAvatar Color="Color.Primary" Image="@GetUserInitials()" Class="cursor-pointer">
@GetFirstLetter(context.User.Identity?.Name)
</MudAvatar>
</ActivatorContent>
<ChildContent>
<MudMenuItem>
<MudText Typo="Typo.body2">
<strong>@context.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> </Authorized>
</AuthorizeView> </AuthorizeView>
</MudAppBar> </MudAppBar>
<MudDrawer Open="@navOpen" Variant="DrawerVariant.Responsive" Elevation="1"> <!-- 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> <MudNavMenu>
<NavMenu /> <NavMenu />
</MudNavMenu> </MudNavMenu>
<div style="padding: 16px; border-top: 1px solid var(--mud-palette-lines-default);">
<MudText Typo="Typo.caption">QuantEngine v@appVersion</MudText> <!-- Drawer Footer -->
<MudText Typo="Typo.caption">배포: @buildTime</MudText> <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> </div>
</MudDrawer> </MudDrawer>
<MudMainContent> <!-- Main Content Area -->
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4"> <MudMainContent Class="mud-main-content-enhanced">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-6">
@Body @Body
</MudContainer> </MudContainer>
</MudMainContent> </MudMainContent>
@@ -35,8 +101,20 @@
@code { @code {
private bool navOpen = true; private bool navOpen = true;
private bool fixedOpen = true;
private bool isDarkMode = false;
private string appVersion = "Local Debug"; private string appVersion = "Local Debug";
private string buildTime = "N/A"; private string buildTime = "N/A";
private MudTheme currentTheme = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isDarkMode = await MudThemeProvider.GetDarkModeAsync() ?? false;
StateHasChanged();
}
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -52,6 +130,19 @@
catch catch
{ {
} }
await base.OnInitializedAsync();
}
private async Task ToggleDarkMode()
{
isDarkMode = !isDarkMode;
await MudThemeProvider.SetDarkModeAsync(isDarkMode);
}
private void ToggleDrawer()
{
navOpen = !navOpen;
} }
private async Task HandleLogoutAsync() private async Task HandleLogoutAsync()
@@ -61,6 +152,16 @@
NavigationManager.NavigateTo("/login"); NavigationManager.NavigateTo("/login");
} }
private string GetFirstLetter(string? name)
{
return string.IsNullOrEmpty(name) ? "?" : name[0].ToString().ToUpper();
}
private string GetUserInitials()
{
return string.Empty;
}
private class VersionInfo private class VersionInfo
{ {
public string? Version { get; set; } public string? Version { get; set; }
@@ -1,81 +1,83 @@
.page { /* 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; position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
} }
} }
@media (min-width: 641px) { @media (min-width: 600px) {
.page { .mud-drawer-footer {
flex-direction: row; position: absolute;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
} }
} }
/* Error UI */
#blazor-error-ui { #blazor-error-ui {
color-scheme: light only; color-scheme: light only;
background: lightyellow; background: lightyellow;
@@ -96,3 +98,8 @@ main {
right: 0.75rem; right: 0.75rem;
top: 0.5rem; top: 0.5rem;
} }
/* Dark Mode Transitions */
* {
transition: background-color 0.3s ease, color 0.3s ease;
}