diff --git a/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor b/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor
new file mode 100644
index 0000000..786e962
--- /dev/null
+++ b/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor
@@ -0,0 +1,258 @@
+@page "/admin/consulting-activities"
+@using TaxBaik.Web.Services.AdminClients
+@inject IConsultingActivityBrowserClient ActivityClient
+@inject IClientBrowserClient ClientClient
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@attribute [Authorize]
+
+상담 활동 관리
+
+
+
+
+ @if (activities == null)
+ {
+
+ }
+ else if (activities.Count == 0)
+ {
+
상담 활동이 없습니다.
+ }
+ else
+ {
+
+
+
+
+
+ @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
+ {
+
+ @clientName
+
+ }
+
+
+
+
+
+
+ @{
+ var desc = context.Item.Description ?? "";
+ if (desc.Length > 30) desc = desc.Substring(0, 30) + "...";
+ }
+ @desc
+
+
+
+
+ @if (context.Item.NextFollowupDate.HasValue)
+ {
+ var daysLeft = (context.Item.NextFollowupDate.Value.Date - DateTime.Today).Days;
+
+ @context.Item.NextFollowupDate.Value.ToString("yyyy-MM-dd")
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+ @(editingActivity == null ? "새 활동 기록" : "활동 기록 수정")
+
+
+
+
+ @foreach (var client in clients)
+ {
+ @client.CompanyName
+ }
+
+
+
+
+
+
+
+
+ 취소
+ 저장
+
+
+
+@code {
+ private List? activities;
+ private List clients = [];
+ private Dictionary clientMap = new();
+ private MudForm? form;
+ private bool isDialogOpen;
+ private ConsultingActivity? editingActivity;
+ private ConsultingActivityForm activityForm = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadData();
+ }
+
+ private async Task LoadData()
+ {
+ try
+ {
+ activities = await ActivityClient.GetAllAsync();
+ var (clientItems, _) = await ClientClient.GetPagedAsync();
+ clients = clientItems.ToList();
+ clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void OpenCreateDialog()
+ {
+ editingActivity = null;
+ activityForm = new ConsultingActivityForm { ActivityDate = DateTime.Now };
+ 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()
+ {
+ 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();
+ parameters.Add("Title", "삭제 확인");
+ parameters.Add("Message", "이 활동을 삭제하시겠습니까?");
+
+ var dialog = await DialogService.ShowAsync("", 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 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; }
+ }
+}
+
+
diff --git a/TaxBaik.Web/Components/Admin/Pages/Contracts.razor b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor
new file mode 100644
index 0000000..7e605a0
--- /dev/null
+++ b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor
@@ -0,0 +1,237 @@
+@page "/admin/contracts"
+@using TaxBaik.Web.Services.AdminClients
+@inject IContractBrowserClient ContractClient
+@inject IClientBrowserClient ClientClient
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@attribute [Authorize]
+
+계약 관리
+
+
+
+
+ @if (contracts == null)
+ {
+
+ }
+ else if (contracts.Count == 0)
+ {
+
계약이 없습니다.
+ }
+ else
+ {
+
+
+
+
+
+ @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
+ {
+
+ @clientName
+
+ }
+
+
+
+
+
+
+
+ @context.Item.StartDate.ToString("yyyy-MM-dd")
+ @if (context.Item.EndDate.HasValue)
+ {
+ ~@context.Item.EndDate.Value.ToString("yyyy-MM-dd")
+ }
+
+
+
+
+ @{
+ var isActive = !context.Item.EndDate.HasValue || context.Item.EndDate.Value >= DateTime.Today;
+ }
+ @if (isActive)
+ {
+ 활성
+ }
+ else
+ {
+ 만료
+ }
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+ 새 계약 추가
+
+
+
+
+ @foreach (var client in clients)
+ {
+ @client.CompanyName
+ }
+
+
+
+
+
+
+
+
+ 취소
+ 저장
+
+
+
+@code {
+ private List? contracts;
+ private List clients = [];
+ private Dictionary clientMap = new();
+ private decimal mrr = 0;
+ private MudForm? form;
+ private bool isDialogOpen;
+ private ContractForm contractForm = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadData();
+ }
+
+ private async Task LoadData()
+ {
+ try
+ {
+ contracts = await ContractClient.GetAllAsync();
+ var (clientItems, _) = await ClientClient.GetPagedAsync();
+ clients = clientItems.ToList();
+ clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
+ mrr = await ContractClient.GetMonthlyRecurringRevenueAsync();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void OpenCreateDialog()
+ {
+ contractForm = new();
+ isDialogOpen = true;
+ }
+
+ private async Task SaveContract()
+ {
+ try
+ {
+ var newId = await ContractClient.CreateAsync(
+ contractForm.ClientId,
+ contractForm.ContractNumber,
+ contractForm.ServiceType,
+ contractForm.StartDate ?? DateTime.Now,
+ contractForm.MonthlyFee);
+
+ if (newId > 0)
+ {
+ Snackbar.Add("계약이 추가되었습니다.", Severity.Success);
+ CloseDialog();
+ await LoadData();
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task DeleteContract(int id)
+ {
+ var parameters = new DialogParameters();
+ parameters.Add("Title", "삭제 확인");
+ parameters.Add("Message", "이 계약을 삭제하시겠습니까?");
+
+ var dialog = await DialogService.ShowAsync("", parameters);
+ var result = await dialog.Result;
+
+ if (result?.Canceled ?? true)
+ return;
+
+ try
+ {
+ await ContractClient.DeleteAsync(id);
+ Snackbar.Add("계약이 삭제되었습니다.", Severity.Success);
+ await LoadData();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void CloseDialog()
+ {
+ isDialogOpen = false;
+ contractForm = new();
+ }
+
+ private class ContractForm
+ {
+ public int ClientId { get; set; }
+ public string ContractNumber { get; set; } = "";
+ public string ServiceType { get; set; } = "";
+ public DateTime? StartDate { get; set; }
+ public decimal? MonthlyFee { get; set; }
+ }
+}
+
+
diff --git a/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor b/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor
new file mode 100644
index 0000000..c1410cf
--- /dev/null
+++ b/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor
@@ -0,0 +1,237 @@
+@page "/admin/revenue-trackings"
+@using TaxBaik.Web.Services.AdminClients
+@inject IRevenueTrackingBrowserClient RevenueClient
+@inject IClientBrowserClient ClientClient
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@attribute [Authorize]
+
+수익 추적 관리
+
+
+
+
+ @if (revenues == null)
+ {
+
+ }
+ else if (revenues.Count == 0)
+ {
+
청구 기록이 없습니다.
+ }
+ else
+ {
+
+
+
+
+
+ @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
+ {
+
+ @clientName
+
+ }
+
+
+
+
+
+
+
+ @if (context.Item.PaymentStatus == "paid")
+ {
+ 납부
+ }
+ else
+ {
+ 미납
+ }
+
+
+
+
+
+ @if (context.Item.PaymentStatus != "paid")
+ {
+
+ }
+
+
+
+
+
+
+ }
+
+
+
+
+
+ 새 청구 추가
+
+
+
+
+ @foreach (var client in clients)
+ {
+ @client.CompanyName
+ }
+
+
+
+
+
+
+
+
+
+ 취소
+ 저장
+
+
+
+@code {
+ private List? revenues;
+ private List clients = [];
+ private Dictionary clientMap = new();
+ private MudForm? form;
+ private bool isDialogOpen;
+ private RevenueForm revenueForm = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadData();
+ }
+
+ private async Task LoadData()
+ {
+ try
+ {
+ revenues = await RevenueClient.GetAllAsync();
+ var (clientItems, _) = await ClientClient.GetPagedAsync();
+ clients = clientItems.ToList();
+ clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void OpenCreateDialog()
+ {
+ revenueForm = new();
+ isDialogOpen = true;
+ }
+
+ private async Task SaveRevenue()
+ {
+ try
+ {
+ var newId = await RevenueClient.CreateAsync(
+ revenueForm.ClientId,
+ revenueForm.InvoiceNumber,
+ revenueForm.InvoiceDate ?? DateTime.Now,
+ revenueForm.Amount,
+ revenueForm.ServiceType,
+ revenueForm.DueDate);
+
+ if (newId > 0)
+ {
+ Snackbar.Add("청구가 추가되었습니다.", Severity.Success);
+ CloseDialog();
+ await LoadData();
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task MarkPaid(int id)
+ {
+ try
+ {
+ await RevenueClient.MarkPaidAsync(id, DateTime.Now);
+ Snackbar.Add("납부가 처리되었습니다.", Severity.Success);
+ await LoadData();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"처리 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task DeleteRevenue(int id)
+ {
+ var parameters = new DialogParameters();
+ parameters.Add("Title", "삭제 확인");
+ parameters.Add("Message", "이 청구를 삭제하시겠습니까?");
+
+ var dialog = await DialogService.ShowAsync("", parameters);
+ var result = await dialog.Result;
+
+ if (result?.Canceled ?? true)
+ return;
+
+ try
+ {
+ await RevenueClient.DeleteAsync(id);
+ Snackbar.Add("청구가 삭제되었습니다.", Severity.Success);
+ await LoadData();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void CloseDialog()
+ {
+ isDialogOpen = false;
+ revenueForm = new();
+ }
+
+ private class RevenueForm
+ {
+ public int ClientId { get; set; }
+ public string InvoiceNumber { get; set; } = "";
+ public DateTime? InvoiceDate { get; set; }
+ public decimal Amount { get; set; }
+ public string? ServiceType { get; set; }
+ public DateTime? DueDate { get; set; }
+ }
+}
+
+
diff --git a/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor b/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor
new file mode 100644
index 0000000..886686d
--- /dev/null
+++ b/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor
@@ -0,0 +1,249 @@
+@page "/admin/tax-filing-schedules"
+@using TaxBaik.Web.Services.AdminClients
+@inject ITaxFilingScheduleBrowserClient TaxFilingClient
+@inject IClientBrowserClient ClientClient
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@attribute [Authorize]
+
+신고 일정 관리
+
+
+
+
+ @if (schedules == null)
+ {
+
+ }
+ else if (schedules.Count == 0)
+ {
+
신고 일정이 없습니다.
+ }
+ else
+ {
+
+
+
+
+
+ @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
+ {
+
+ @clientName
+
+ }
+
+
+
+
+
+ @{
+ var daysLeft = (context.Item.DueDate.Date - DateTime.Today).Days;
+ var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success;
+ }
+
+ @context.Item.DueDate.ToString("yyyy-MM-dd")
+ @if (daysLeft >= 0) { (D-@daysLeft) }
+ else { (마감@(Math.Abs(daysLeft))일경과) }
+
+
+
+
+
+
+ @if (context.Item.Status == "completed")
+ {
+ 완료
+ }
+ else
+ {
+ 대기
+ }
+
+
+
+
+
+ @if (context.Item.Status != "completed")
+ {
+
+ }
+
+
+
+
+
+
+ }
+
+
+
+
+
+ @(editingSchedule == null ? "새 신고 일정 추가" : "신고 일정 수정")
+
+
+
+
+ @foreach (var client in clients)
+ {
+ @client.CompanyName
+ }
+
+
+
+
+
+
+
+ 취소
+ 저장
+
+
+
+@code {
+ private List? schedules;
+ private List clients = [];
+ private Dictionary clientMap = new();
+ private MudForm? form;
+ private bool isDialogOpen;
+ private TaxFilingSchedule? editingSchedule;
+ private TaxFilingScheduleForm scheduleForm = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadData();
+ }
+
+ private async Task LoadData()
+ {
+ try
+ {
+ schedules = await TaxFilingClient.GetAllAsync();
+ var (clientItems, _) = await ClientClient.GetPagedAsync();
+ clients = clientItems.ToList();
+ clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void OpenCreateDialog()
+ {
+ editingSchedule = null;
+ scheduleForm = new();
+ isDialogOpen = true;
+ }
+
+ private async Task SaveSchedule()
+ {
+ try
+ {
+ if (editingSchedule == null)
+ {
+ var newId = await TaxFilingClient.CreateAsync(
+ scheduleForm.ClientId,
+ scheduleForm.FilingType,
+ scheduleForm.DueDate ?? DateTime.Now,
+ scheduleForm.FilingYear);
+
+ if (newId > 0)
+ {
+ Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success);
+ CloseDialog();
+ await LoadData();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task CompleteSchedule(int id)
+ {
+ try
+ {
+ await TaxFilingClient.MarkCompletedAsync(id);
+ Snackbar.Add("신고 일정이 완료 처리되었습니다.", Severity.Success);
+ await LoadData();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"처리 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task DeleteSchedule(int id)
+ {
+ var parameters = new DialogParameters();
+ parameters.Add("Title", "삭제 확인");
+ parameters.Add("Message", "이 신고 일정을 삭제하시겠습니까?");
+
+ var dialog = await DialogService.ShowAsync("", parameters);
+ var result = await dialog.Result;
+
+ if (result?.Canceled ?? true)
+ return;
+
+ try
+ {
+ await TaxFilingClient.DeleteAsync(id);
+ Snackbar.Add("신고 일정이 삭제되었습니다.", Severity.Success);
+ await LoadData();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private void CloseDialog()
+ {
+ isDialogOpen = false;
+ editingSchedule = null;
+ scheduleForm = new();
+ }
+
+ private class TaxFilingScheduleForm
+ {
+ public int ClientId { get; set; }
+ public string FilingType { get; set; } = "";
+ public DateTime? DueDate { get; set; }
+ public int FilingYear { get; set; } = DateTime.Now.Year;
+ }
+}
+
+