lite blazor 데이터 갱신 정리
TaxBaik CI/CD / build-and-deploy (push) Successful in 54s

This commit is contained in:
2026-06-29 18:18:04 +09:00
parent 682e2db3a3
commit 3be379431f
10 changed files with 81 additions and 207 deletions
@@ -100,7 +100,7 @@
<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">@client.CompanyName</MudSelectItem>
<MudSelectItem Value="@client.Id">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="activityForm.ActivityType" Label="활동 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
@@ -155,9 +155,9 @@
try
{
activities = await ActivityClient.GetAllAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
clients = clientItems.ToList();
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
}
catch (Exception ex)
{
@@ -273,6 +273,12 @@
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; }
@@ -110,7 +110,7 @@
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@(string.IsNullOrEmpty(client.CompanyName) ? client.Name : client.CompanyName)</MudSelectItem>
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
@@ -165,9 +165,9 @@
try
{
contracts = await ContractClient.GetAllAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
clients = clientItems.ToList();
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
mrr = await ContractClient.GetMonthlyRecurringRevenueAsync();
}
catch (Exception ex)
@@ -253,6 +253,12 @@
contractForm = new();
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class ContractForm
{
public int? ClientId { get; set; }
@@ -96,7 +96,7 @@
<MudSelect T="int" @bind-Value="revenueForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
@foreach (var client in clients)
{
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
<MudSelectItem Value="@client.Id">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudTextField T="string" @bind-Value="revenueForm.InvoiceNumber" Label="청구번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
@@ -150,9 +150,9 @@
try
{
revenues = await RevenueClient.GetAllAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
clients = clientItems.ToList();
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
}
catch (Exception ex)
{
@@ -252,6 +252,12 @@
revenueForm = new();
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class RevenueForm
{
public int ClientId { get; set; }
@@ -127,7 +127,7 @@
RequiredError="고객을 선택하세요.">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@(string.IsNullOrEmpty(client.CompanyName) ? client.Name : client.CompanyName)</MudSelectItem>
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
@@ -182,9 +182,9 @@
try
{
schedules = await TaxFilingClient.GetAllAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
clients = clientItems.ToList();
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
}
catch (Exception ex)
{
@@ -286,9 +286,15 @@
scheduleForm = new();
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class TaxFilingScheduleForm
{
public int ClientId { get; set; }
public int? ClientId { get; set; }
public string FilingType { get; set; } = "";
public DateTime? DueDate { get; set; }
public int FilingYear { get; set; } = DateTime.Now.Year;
@@ -101,7 +101,7 @@
{
try
{
var (items, _) = await ClientClient.GetPagedAsync(1, 20, search: value);
var (items, _) = await ClientClient.GetPagedAsync(1, 100, search: value);
return items;
}
catch
@@ -110,6 +110,12 @@
}
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private async Task AddFiling()
{
try
@@ -87,7 +87,7 @@ else
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@(string.IsNullOrEmpty(client.CompanyName) ? client.Name : client.CompanyName)</MudSelectItem>
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
@@ -150,9 +150,9 @@ else
try
{
profiles = await TaxProfileClient.GetAllAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync();
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
clients = clientItems.ToList();
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
}
catch (Exception ex)
{
@@ -195,7 +195,7 @@ else
await form.Validate();
if (!form.IsValid)
{
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
return;
}
}
@@ -210,8 +210,13 @@ else
}
else
{
if (!profileForm.ClientId.HasValue)
{
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
return;
}
var newId = await TaxProfileClient.CreateAsync(
profileForm.ClientId,
profileForm.ClientId.Value,
profileForm.BusinessType);
if (newId > 0)
{
@@ -274,9 +279,15 @@ else
_ => Color.Default
};
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class TaxProfileForm
{
public int ClientId { get; set; }
public int? ClientId { get; set; }
public string BusinessType { get; set; } = "";
public string TaxRiskLevel { get; set; } = "normal";
public DateTime? NextFilingDueDate { get; set; }
-87
View File
@@ -1,87 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace TaxBaik.Web.Hubs;
/// <summary>
/// Real-time notification hub for admin dashboard
/// SOLID: Single Responsibility - Only broadcasts change notifications
/// No state management - stateless broadcast pattern
/// </summary>
[Authorize]
public class NotificationHub : Hub
{
private const string AdminGroup = "admins";
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, AdminGroup);
await base.OnConnectedAsync();
}
/// <summary>
/// Broadcast inquiry status changed to all connected admins
/// Clients should re-fetch from API to verify
/// </summary>
public async Task NotifyInquiryStatusChanged(int inquiryId, string newStatus)
{
await Clients.Group(AdminGroup).SendAsync("InquiryStatusChanged", new
{
InquiryId = inquiryId,
Status = newStatus,
ChangedAt = DateTime.UtcNow
});
}
/// <summary>
/// Broadcast inquiry submitted (new inquiry created)
/// </summary>
public async Task NotifyInquiryCreated(int inquiryId, string name)
{
await Clients.Group(AdminGroup).SendAsync("InquiryCreated", new
{
InquiryId = inquiryId,
Name = name,
CreatedAt = DateTime.UtcNow
});
}
/// <summary>
/// Broadcast client created
/// </summary>
public async Task NotifyClientCreated(int clientId, string name)
{
await Clients.Group(AdminGroup).SendAsync("ClientCreated", new
{
ClientId = clientId,
Name = name,
CreatedAt = DateTime.UtcNow
});
}
/// <summary>
/// Broadcast announcement published
/// </summary>
public async Task NotifyAnnouncementPublished(int announcementId, string title)
{
await Clients.Group(AdminGroup).SendAsync("AnnouncementPublished", new
{
AnnouncementId = announcementId,
Title = title,
PublishedAt = DateTime.UtcNow
});
}
/// <summary>
/// Broadcast tax filing completed
/// </summary>
public async Task NotifyFilingCompleted(int filingId, string filingType)
{
await Clients.Group(AdminGroup).SendAsync("FilingCompleted", new
{
FilingId = filingId,
FilingType = filingType,
CompletedAt = DateTime.UtcNow
});
}
}
-8
View File
@@ -52,9 +52,6 @@ builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
// SignalR (Notifications only, no state management)
builder.Services.AddSignalR();
// Razor Pages + Blazor Server 통합
builder.Services.AddRazorPages();
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
@@ -197,9 +194,6 @@ builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorization();
builder.Services.AddAuthorizationCore();
// Notifications (SignalR)
builder.Services.AddScoped<INotificationService, NotificationService>();
// Telegram Notification
builder.Services.AddHttpClient<ITelegramNotificationService, TelegramNotificationService>();
@@ -350,8 +344,6 @@ app.MapControllers();
app.MapHealthChecks("/healthz");
app.MapRazorPages();
// SignalR Hub
app.MapHub<TaxBaik.Web.Hubs.NotificationHub>("/taxbaik/notifications");
// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다.
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
@@ -1,72 +0,0 @@
namespace TaxBaik.Web.Services;
/// <summary>
/// Notification service for real-time admin updates
/// SOLID: Single Responsibility - Event notification only
/// Uses Blazor Server's built-in SignalR for real-time communication
/// </summary>
public interface INotificationService
{
event Func<int, string, Task>? OnInquiryStatusChanged;
event Func<int, string, Task>? OnInquiryCreated;
event Func<int, string, Task>? OnClientCreated;
event Func<int, string, Task>? OnAnnouncementPublished;
event Func<int, string, Task>? OnFilingCompleted;
Task TriggerInquiryStatusChanged(int inquiryId, string status);
Task TriggerInquiryCreated(int inquiryId, string name);
Task TriggerClientCreated(int clientId, string name);
Task TriggerAnnouncementPublished(int announcementId, string title);
Task TriggerFilingCompleted(int filingId, string filingType);
}
public class NotificationService : INotificationService
{
private readonly ILogger<NotificationService> _logger;
public NotificationService(ILogger<NotificationService> logger)
{
_logger = logger;
}
public event Func<int, string, Task>? OnInquiryStatusChanged;
public event Func<int, string, Task>? OnInquiryCreated;
public event Func<int, string, Task>? OnClientCreated;
public event Func<int, string, Task>? OnAnnouncementPublished;
public event Func<int, string, Task>? OnFilingCompleted;
public async Task TriggerInquiryStatusChanged(int inquiryId, string status)
{
_logger.LogInformation($"Inquiry {inquiryId} status changed to {status}");
if (OnInquiryStatusChanged != null)
await OnInquiryStatusChanged(inquiryId, status);
}
public async Task TriggerInquiryCreated(int inquiryId, string name)
{
_logger.LogInformation($"New inquiry {inquiryId} from {name}");
if (OnInquiryCreated != null)
await OnInquiryCreated(inquiryId, name);
}
public async Task TriggerClientCreated(int clientId, string name)
{
_logger.LogInformation($"New client {clientId}: {name}");
if (OnClientCreated != null)
await OnClientCreated(clientId, name);
}
public async Task TriggerAnnouncementPublished(int announcementId, string title)
{
_logger.LogInformation($"Announcement {announcementId} published: {title}");
if (OnAnnouncementPublished != null)
await OnAnnouncementPublished(announcementId, title);
}
public async Task TriggerFilingCompleted(int filingId, string filingType)
{
_logger.LogInformation($"Filing {filingId} ({filingType}) completed");
if (OnFilingCompleted != null)
await OnFilingCompleted(filingId, filingType);
}
}