diff --git a/src/dotnet/QuantEngine.Web/Client/Components/ConfirmDialog.razor b/src/dotnet/QuantEngine.Web/Client/Components/ConfirmDialog.razor new file mode 100644 index 0000000..e8d8b86 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Components/ConfirmDialog.razor @@ -0,0 +1,61 @@ +@namespace QuantEngine.Web.Client.Components +@inject IDialogService DialogService + +@code { + public static async Task Show(IDialogService dialogService, string title, string message, string confirmText = "확인", string cancelText = "취소") + { + var options = new DialogOptions + { + CloseButton = false, + MaxWidth = MaxWidth.Small, + FullWidth = true, + DisableBackdropClick = true + }; + + var parameters = new DialogParameters + { + { x => x.Title, title }, + { x => x.Message, message }, + { x => x.ConfirmText, confirmText }, + { x => x.CancelText, cancelText } + }; + + var dialog = await dialogService.ShowAsync(title, parameters, options); + var result = await dialog.Result; + + return !result.Cancelled && (bool?)result.Data == true; + } +} + + + + + @Title + @Message + + + + @CancelText + @ConfirmText + + + +@code { + [CascadingParameter] + private MudDialogInstance 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(); +} diff --git a/src/dotnet/QuantEngine.Web/Client/Components/FormField.razor b/src/dotnet/QuantEngine.Web/Client/Components/FormField.razor new file mode 100644 index 0000000..b5e00d9 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Components/FormField.razor @@ -0,0 +1,125 @@ +@namespace QuantEngine.Web.Client.Components + + + + + @switch (Type) + { + case "text": + case "email": + case "password": + case "number": + + break; + + case "textarea": + + break; + + case "select": + + @foreach (var option in Options) + { + @option + } + + break; + + case "checkbox": + + @Label + + break; + + case "date": + + break; + } + + @if (!string.IsNullOrEmpty(HelpText)) + { + @HelpText + } + + +@code { + [Parameter] + public string Label { get; set; } = ""; + + [Parameter] + public string Type { get; set; } = "text"; + + [Parameter] + public string Value { get; set; } = ""; + + [Parameter] + public EventCallback 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 Options { get; set; } = new(); +} + + diff --git a/src/dotnet/QuantEngine.Web/Client/Pages/Portfolio.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Portfolio.razor new file mode 100644 index 0000000..8842a4d --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Portfolio.razor @@ -0,0 +1,238 @@ +@page "/portfolio" +@attribute [Authorize] +@inject HttpClient Http + +QuantEngine - 포트폴리오 + + +
+ 포트폴리오 + 자산 구성 및 성과 분석 +
+ + + + + + 총 평가액 + ₩125.5M + +3.2% (이번 달) + + + + + + 보유 종목 + 12개 + 주식 및 펀드 + + + + + + 수익률 + +8.5% + 연간 기준 + + + + + + 위험도 + 중간 + + Moderate + + + + + + + + + + 자산 구성 + + + + 종목/펀드명 + 수량 + 현재가 + 평가액 + 수익률 + 비율 + + + +
+ @context.Name[0] +
+ @context.Name + @context.Ticker +
+
+
+ + @context.Quantity.ToString("N0") + + + ₩@context.CurrentPrice.ToString("N0") + + + ₩@context.Value.ToString("N0") + + + + @(context.ReturnRate >= 0 ? "+" : "")@context.ReturnRate.ToString("F1")% + + + + @context.Ratio.ToString("F1")% + +
+
+
+
+ + + + 자산 분류 + + + @foreach (var category in AssetCategories) + { +
+
+ @category.Name + @category.Percentage% +
+ +
+ } +
+
+
+
+ + + + 거래 이력 + + @if (TradingHistory.Count == 0) + { + 거래 이력이 없습니다. + } + else + { + + + 일자 + 종목 + 구분 + 수량 + 단가 + 금액 + 수수료 + + + + @context.Date.ToString("yyyy-MM-dd") + + + @context.Ticker + + + + @context.Type + + + + @context.Quantity + + + ₩@context.Price.ToString("N0") + + + ₩@context.Amount.ToString("N0") + + + ₩@context.Fee.ToString("N0") + + + + } + + +@code { + private List Assets = new(); + private List AssetCategories = new(); + private List TradingHistory = new(); + + protected override async Task OnInitializedAsync() + { + await LoadAssets(); + } + + private async Task LoadAssets() + { + Assets = new List + { + new AssetModel { Name = "삼성전자", Ticker = "005930", Quantity = 50, CurrentPrice = 70000, Value = 3500000, ReturnRate = 5.2, Ratio = 28.0 }, + new AssetModel { Name = "LG화학", Ticker = "051910", Quantity = 30, CurrentPrice = 820000, Value = 24600000, ReturnRate = -2.1, Ratio = 19.6 }, + new AssetModel { Name = "현대차", Ticker = "005380", Quantity = 40, CurrentPrice = 245000, Value = 9800000, ReturnRate = 8.5, Ratio = 7.8 }, + new AssetModel { Name = "SK하이닉스", Ticker = "000660", Quantity = 25, CurrentPrice = 105000, Value = 2625000, ReturnRate = 12.3, Ratio = 2.1 }, + new AssetModel { Name = "삼성중공업", Ticker = "010140", Quantity = 60, CurrentPrice = 85000, Value = 5100000, ReturnRate = 3.7, Ratio = 4.1 }, + new AssetModel { Name = "포스코", Ticker = "005490", Quantity = 20, CurrentPrice = 75000, Value = 1500000, ReturnRate = -5.2, Ratio = 1.2 }, + }; + + AssetCategories = new List + { + 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 + { + 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; } + } +} diff --git a/src/dotnet/QuantEngine.Web/Client/Program.cs b/src/dotnet/QuantEngine.Web/Client/Program.cs index f7a2fc4..17c8629 100644 --- a/src/dotnet/QuantEngine.Web/Client/Program.cs +++ b/src/dotnet/QuantEngine.Web/Client/Program.cs @@ -8,6 +8,9 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); // Register LocalStorage for cross-platform session persistence builder.Services.AddScoped(); +// App State Service (RBAC & global state management) +builder.Services.AddScoped(); + // Authentication setup in WebAssembly client builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); diff --git a/src/dotnet/QuantEngine.Web/Client/Services/AppStateService.cs b/src/dotnet/QuantEngine.Web/Client/Services/AppStateService.cs new file mode 100644 index 0000000..0c54372 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Services/AppStateService.cs @@ -0,0 +1,142 @@ +namespace QuantEngine.Web.Client.Services; + +public class AppStateService +{ + private UserContext _currentUser; + private List _userRoles = new(); + private bool _isInitialized = false; + + public event Action OnStateChanged; + + public UserContext CurrentUser + { + get => _currentUser; + set + { + _currentUser = value; + NotifyStateChanged(); + } + } + + public List UserRoles + { + get => _userRoles; + set + { + _userRoles = value; + NotifyStateChanged(); + } + } + + public bool IsInitialized + { + get => _isInitialized; + set + { + _isInitialized = value; + NotifyStateChanged(); + } + } + + public AppStateService() + { + _currentUser = new UserContext(); + _userRoles = new List(); + } + + /// + /// Initialize app state from current user context + /// + 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 { "Admin" }; + } + } + catch + { + // Handle error + } + finally + { + IsInitialized = true; + } + } + + /// + /// Check if user has specific role (RBAC) + /// + public bool HasRole(string role) + { + return UserRoles.Contains(role); + } + + /// + /// Check if user has any of the specified roles + /// + public bool HasAnyRole(params string[] roles) + { + return roles.Any(r => UserRoles.Contains(r)); + } + + /// + /// Check if user has all specified roles + /// + public bool HasAllRoles(params string[] roles) + { + return roles.All(r => UserRoles.Contains(r)); + } + + /// + /// Clear user state + /// + public void Clear() + { + CurrentUser = new UserContext(); + UserRoles = new List(); + IsInitialized = false; + } + + private void NotifyStateChanged() => OnStateChanged?.Invoke(); +} + +/// +/// User context model +/// +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; +} + +/// +/// API Response wrapper +/// +public class ApiResponse +{ + public bool Success { get; set; } + public string Message { get; set; } + public T Data { get; set; } +} + +/// +/// Pagination model +/// +public class PaginatedResponse +{ + public List 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; +}