refactor: complete WebAssembly migration - proper architecture
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m17s
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m17s
Phase 8: Complete WebAssembly 렌더 모드 전환 (정공법) Migration Summary: - ALL Admin components → TaxBaik.Web.Client - Routes.razor, Pages/*, Layout/*, Shared/*, Forms/* - App.razor → TaxBaik.WasmClient (호스트 컴포넌트) - Shared utilities → TaxBaik.Application.Utils Architecture: ✅ App.razor: TaxBaik.WasmClient (WebAssembly, 호스트) ✅ Routes + Pages: TaxBaik.WasmClient (WebAssembly) ✅ Layout + Shared + Forms: TaxBaik.WasmClient (WebAssembly) ✅ Services: TaxBaik.Web (API-First) Key Changes: - Namespaces: TaxBaik.Web.Components.Admin → TaxBaik.WasmClient.Components.Admin - Shared utilities: TaxBaik.Application.Utils (single source of truth) - Program.cs: MapRazorComponents<TaxBaik.WasmClient.Components.Admin.App>() - _Imports.razor: Components/Admin 폴더에 재구성 Build Status: ✅ 0 errors, 0 warnings Benefits: - Stateless server (no Circuit memory) - Client-side rendering (WebAssembly) - Unlimited concurrent users (horizontal scaling) - ERP-ready architecture Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
@page "/admin/consulting-activities"
|
||||
@using TaxBaik.Web.Services.AdminClients
|
||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||
@inject IConsultingActivityBrowserClient ActivityClient
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>상담 활동 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">상담 활동 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 상담 이력과 팔로업을 추적합니다.</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
|
||||
새 활동 기록
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<AdminDataPanel Loading="@(activities is null)" SkeletonContent="@ActivitySkeleton">
|
||||
@if (activities is null)
|
||||
{
|
||||
}
|
||||
else if (activities.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-4">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Timeline" Class="me-2" />
|
||||
상담 활동이 없습니다.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="ConsultingActivity"
|
||||
Items="@activities"
|
||||
Dense="true"
|
||||
Hover="true"
|
||||
Striped="true"
|
||||
Virtualize="true"
|
||||
RowsPerPage="30"
|
||||
Class="admin-grid">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
||||
<TemplateColumn Title="고객">
|
||||
<CellTemplate>
|
||||
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
||||
{
|
||||
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
|
||||
@clientName
|
||||
</MudLink>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.ActivityType" Title="활동 유형" />
|
||||
<PropertyColumn Property="x => x.ActivityDate" Title="활동일시" Format="g" />
|
||||
<TemplateColumn Title="설명">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var desc = context.Item.Description ?? "";
|
||||
if (desc.Length > 30) desc = desc.Substring(0, 30) + "...";
|
||||
}
|
||||
<span>@desc</span>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="다음 팔로업">
|
||||
<CellTemplate>
|
||||
@if (context.Item.NextFollowupDate.HasValue)
|
||||
{
|
||||
var daysLeft = (context.Item.NextFollowupDate.Value.Date - DateTime.Today).Days;
|
||||
<MudChip Size="Size.Small"
|
||||
Color="@(daysLeft < 0 ? Color.Error : daysLeft <= 3 ? Color.Warning : Color.Success)"
|
||||
Variant="Variant.Filled">
|
||||
@context.Item.NextFollowupDate.Value.ToString("yyyy-MM-dd")
|
||||
</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="작업" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" OnClick="@(async () => await OpenEditDialog(context.Item))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
|
||||
OnClick="@(async () => await DeleteActivity(context.Item.Id))" />
|
||||
</MudButtonGroup>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</AdminDataPanel>
|
||||
|
||||
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(editingActivity == null ? "새 활동 기록" : "활동 기록 수정")</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudForm @ref="form">
|
||||
<MudSelect T="int" @bind-Value="activityForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||
@foreach (var client in clients)
|
||||
{
|
||||
<MudSelectItem Value="@client.Id">@GetClientDisplayName(client)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="activityForm.ActivityType" Group="CONSULTING_ACTIVITY_TYPE" Label="활동 유형" Class="mb-4" Required="true" />
|
||||
<MudDatePicker @bind-Date="activityForm.ActivityDate" Label="활동일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||
<MudTextField T="string" @bind-Value="activityForm.Description" Label="설명" Variant="Variant.Outlined" FullWidth="true" Lines="3" Class="mb-4" Required="true" />
|
||||
<MudDatePicker @bind-Date="activityForm.NextFollowupDate" Label="다음 팔로업일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="CloseDialog">취소</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="SaveActivity">저장</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||
|
||||
private List<ConsultingActivity>? activities;
|
||||
private List<Client> clients = [];
|
||||
private Dictionary<int, string> clientMap = new();
|
||||
private MudForm? form;
|
||||
private bool isDialogOpen;
|
||||
private ConsultingActivity? editingActivity;
|
||||
private ConsultingActivityForm activityForm = new();
|
||||
|
||||
private RenderFragment ActivitySkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
{
|
||||
var authState = await AuthStateTask;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
await LoadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
try
|
||||
{
|
||||
activities = await ActivityClient.GetAllAsync();
|
||||
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||
clients = clientItems.ToList();
|
||||
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenCreateDialog()
|
||||
{
|
||||
editingActivity = null;
|
||||
activityForm = new ConsultingActivityForm
|
||||
{
|
||||
ActivityDate = DateTime.Now,
|
||||
ClientId = clients.FirstOrDefault()?.Id ?? 0
|
||||
};
|
||||
isDialogOpen = true;
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(ConsultingActivity activity)
|
||||
{
|
||||
editingActivity = activity;
|
||||
activityForm = new ConsultingActivityForm
|
||||
{
|
||||
ClientId = activity.ClientId,
|
||||
ActivityType = activity.ActivityType,
|
||||
ActivityDate = activity.ActivityDate,
|
||||
Description = activity.Description,
|
||||
NextFollowupDate = activity.NextFollowupDate
|
||||
};
|
||||
isDialogOpen = true;
|
||||
}
|
||||
|
||||
private async Task SaveActivity()
|
||||
{
|
||||
if (form != null)
|
||||
{
|
||||
await form.Validate();
|
||||
if (!form.IsValid)
|
||||
{
|
||||
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (editingActivity == null)
|
||||
{
|
||||
var actDate = activityForm.ActivityDate ?? DateTime.Now;
|
||||
var newId = await ActivityClient.CreateAsync(
|
||||
activityForm.ClientId,
|
||||
activityForm.ActivityType,
|
||||
actDate,
|
||||
activityForm.Description,
|
||||
null,
|
||||
activityForm.NextFollowupDate);
|
||||
|
||||
if (newId > 0)
|
||||
{
|
||||
Snackbar.Add("활동이 기록되었습니다.", Severity.Success);
|
||||
CloseDialog();
|
||||
await LoadData();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ActivityClient.UpdateAsync(
|
||||
editingActivity.Id,
|
||||
null,
|
||||
activityForm.NextFollowupDate);
|
||||
|
||||
Snackbar.Add("활동이 업데이트되었습니다.", Severity.Success);
|
||||
CloseDialog();
|
||||
await LoadData();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteActivity(int id)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Title", "삭제 확인" },
|
||||
{ "Message", "이 활동을 삭제하시겠습니까?" }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result?.Canceled ?? true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await ActivityClient.DeleteAsync(id);
|
||||
Snackbar.Add("활동이 삭제되었습니다.", Severity.Success);
|
||||
await LoadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
isDialogOpen = false;
|
||||
editingActivity = null;
|
||||
activityForm = new();
|
||||
}
|
||||
|
||||
private static string GetClientDisplayName(Client client)
|
||||
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||
? client.CompanyName
|
||||
: !string.IsNullOrWhiteSpace(client.Name)
|
||||
? client.Name
|
||||
: $"Client #{client.Id}";
|
||||
private class ConsultingActivityForm
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
public string ActivityType { get; set; } = "";
|
||||
public DateTime? ActivityDate { get; set; } = DateTime.Now;
|
||||
public string Description { get; set; } = "";
|
||||
public DateTime? NextFollowupDate { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user