Files
QuantEngineByItz/src/dotnet/QuantEngine.Web/Client/Pages/Users.razor
T
kjh2064 dbb3a78afb
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
FEAT: Migrate Collection and User Auth endpoints to FastEndpoints (API-First), implement MudDialog based Users CRUD (MVVM) and update AGENTS.md guidelines
2026-07-06 10:24:00 +09:00

267 lines
9.7 KiB
Plaintext

@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
{
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.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 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;
}
}