FEAT: Migrate Collection and User Auth endpoints to FastEndpoints (API-First), implement MudDialog based Users CRUD (MVVM) and update AGENTS.md guidelines
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 6s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 10s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 1m5s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 6s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 10s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 1m5s
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
@page "/users"
|
||||
@attribute [Authorize]
|
||||
@using MudBlazor
|
||||
@inject HttpClient Http
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageTitle>QuantEngine - 사용자 관리</PageTitle>
|
||||
|
||||
@@ -32,25 +35,22 @@
|
||||
<MudTable Items="@FilteredUsers" Dense="true" Hover="true" Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>이름</MudTh>
|
||||
<MudTh>이메일</MudTh>
|
||||
<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>
|
||||
<MudText Typo="Typo.body2">@context.Name</MudText>
|
||||
<MudAvatar Size="Size.Small" Color="Color.Primary">@context.Username[0].ToString().ToUpper()</MudAvatar>
|
||||
<MudText Typo="Typo.body2">@context.Username</MudText>
|
||||
</div>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Email">
|
||||
<MudText Typo="Typo.body2">@context.Email</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Role">
|
||||
<MudChip T="string" Label="true" Size="Size.Small"
|
||||
Color="@(context.Role == "Admin" ? Color.Primary : Color.Default)"
|
||||
Color="@(context.Role == "Admin" ? Color.Primary : (context.Role == "Operator" ? Color.Secondary : Color.Default))"
|
||||
Variant="Variant.Filled">
|
||||
@context.Role
|
||||
</MudChip>
|
||||
@@ -63,7 +63,10 @@
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Joined">
|
||||
<MudText Typo="Typo.body2">@context.CreatedDate.ToString("yyyy-MM-dd")</MudText>
|
||||
<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>
|
||||
@@ -74,16 +77,54 @@
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private List<UserModel> _users = new();
|
||||
private string SearchQuery = "";
|
||||
<!-- 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>
|
||||
|
||||
private IEnumerable<UserModel> FilteredUsers
|
||||
@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.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
u.Email.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
|
||||
: _users.Where(u => u.Username.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -95,68 +136,131 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
_users = new List<UserModel>
|
||||
var res = await Http.GetFromJsonAsync<List<UserDto>>("api/users");
|
||||
if (res != null)
|
||||
{
|
||||
new UserModel
|
||||
{
|
||||
Id = "1",
|
||||
Name = "admin",
|
||||
Email = "admin@quantengine.local",
|
||||
Role = "Admin",
|
||||
IsActive = true,
|
||||
CreatedDate = DateTime.Now.AddMonths(-6)
|
||||
},
|
||||
new UserModel
|
||||
{
|
||||
Id = "2",
|
||||
Name = "user1",
|
||||
Email = "user1@example.com",
|
||||
Role = "Viewer",
|
||||
IsActive = true,
|
||||
CreatedDate = DateTime.Now.AddMonths(-3)
|
||||
},
|
||||
new UserModel
|
||||
{
|
||||
Id = "3",
|
||||
Name = "user2",
|
||||
Email = "user2@example.com",
|
||||
Role = "Operator",
|
||||
IsActive = true,
|
||||
CreatedDate = DateTime.Now.AddMonths(-1)
|
||||
}
|
||||
};
|
||||
_users = res;
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle error
|
||||
Snackbar.Add($"사용자 목록 로드 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAddUserDialog()
|
||||
private void OpenAddUserDialog()
|
||||
{
|
||||
// Dialog implementation would go here
|
||||
await Task.CompletedTask;
|
||||
_isEditMode = false;
|
||||
_formModel = new UserFormModel { Role = "Viewer", IsActive = true };
|
||||
_dialogVisible = true;
|
||||
}
|
||||
|
||||
private async Task EditUser(UserModel user)
|
||||
private void EditUser(UserDto user)
|
||||
{
|
||||
// Edit dialog implementation
|
||||
await Task.CompletedTask;
|
||||
_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(UserModel user)
|
||||
private async Task DeleteUser(UserDto user)
|
||||
{
|
||||
// Delete confirmation and implementation
|
||||
await Task.CompletedTask;
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"사용자 삭제",
|
||||
$"정말로 사용자 '{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 class UserModel
|
||||
private void CloseDialog()
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Role { get; set; }
|
||||
_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 DateTime CreatedDate { 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user