diff --git a/TaxBaik.Web/Components/Admin/App.razor b/TaxBaik.Web/Components/Admin/App.razor index 2ecbf0e..c45f15f 100644 --- a/TaxBaik.Web/Components/Admin/App.razor +++ b/TaxBaik.Web/Components/Admin/App.razor @@ -1,4 +1,5 @@ @using Microsoft.AspNetCore.Components.Web +@using Microsoft.FluentUI.AspNetCore.Components @@ -6,9 +7,9 @@ 백원숙 세무회계 - 관리자 - + - + + - -@code { - private bool isDarkMode = false; - private MudTheme mudTheme = new() - { - Palette = new PaletteLight() - { - Primary = "#1976D2", - PrimaryContrastText = "#FFFFFF", - Secondary = "#2D9F7E", - SecondaryContrastText = "#FFFFFF", - Tertiary = "#FF8A50", - TertiaryContrastText = "#FFFFFF", - Surface = "#F5F7FA", - Background = "#FFFFFF", - BackgroundGrey = "#F8F9FB", - DrawerBackground = "#FFFFFF", - DrawerText = "#424242", - AppbarBackground = "#FFFFFF", - AppbarText = "#424242", - TextPrimary = "#1A1A1A", - TextSecondary = "#64748B", - TextDisabled = "#94A3B8", - ActionDefault = "#1976D2", - ActionDisabled = "#BDBDBD", - Divider = "#E2E8F0", - DividerLight = "#F1F5F9", - Error = "#DC2626", - ErrorContrastText = "#FFFFFF", - Warning = "#F59E0B", - WarningContrastText = "#FFFFFF", - Info = "#06B6D4", - InfoContrastText = "#FFFFFF", - Success = "#16A34A", - SuccessContrastText = "#FFFFFF", - }, - LayoutProperties = new LayoutProperties() - { - DefaultBorderRadius = "8px" - }, - Typography = new Typography() - { - Default = new Default() - { - FontSize = ".875rem", - FontWeight = 400, - LineHeight = 1.5 - }, - H1 = new H1() - { - FontSize = "2.5rem", - FontWeight = 600, - LineHeight = 1.2 - }, - H2 = new H2() - { - FontSize = "2rem", - FontWeight = 600, - LineHeight = 1.3 - }, - H3 = new H3() - { - FontSize = "1.75rem", - FontWeight = 600, - LineHeight = 1.3 - }, - H4 = new H4() - { - FontSize = "1.5rem", - FontWeight = 600, - LineHeight = 1.4 - }, - H5 = new H5() - { - FontSize = "1.25rem", - FontWeight = 500, - LineHeight = 1.4 - }, - H6 = new H6() - { - FontSize = "1rem", - FontWeight = 500, - LineHeight = 1.5 - } - } - }; -} diff --git a/TaxBaik.Web/Components/Admin/ConfirmDialog.razor b/TaxBaik.Web/Components/Admin/ConfirmDialog.razor index 876557d..6f61086 100644 --- a/TaxBaik.Web/Components/Admin/ConfirmDialog.razor +++ b/TaxBaik.Web/Components/Admin/ConfirmDialog.razor @@ -1,18 +1,17 @@ -@using MudBlazor - - - - 정말로 삭제하시겠습니까? - - - 취소 - 삭제 - - +@using Microsoft.FluentUI.AspNetCore.Components +
+
삭제 확인
+

정말로 삭제하시겠습니까?

+
+ 취소 + 삭제 +
+
@code { - [CascadingParameter] MudDialogInstance? MudDialog { get; set; } + [Parameter] public EventCallback OnCancel { get; set; } + [Parameter] public EventCallback OnConfirm { get; set; } - void Cancel() => MudDialog?.Cancel(); - void Confirm() => MudDialog?.Close(DialogResult.Ok(true)); + Task Cancel() => OnCancel.InvokeAsync(); + Task Confirm() => OnConfirm.InvokeAsync(); } diff --git a/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor b/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor index 54d653d..3e2473e 100644 --- a/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor +++ b/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor @@ -1,49 +1,28 @@ @using TaxBaik.Application.Services +@using Microsoft.FluentUI.AspNetCore.Components - - - - - - - - - - - - - - - -
- - @ButtonText - - 취소 +
+ + + + + + + +
+ +
- + @code { - [Parameter, EditorRequired] - public string ButtonText { get; set; } = "저장"; - - [Parameter] - public EventCallback OnSubmit { get; set; } - - [Parameter] - public EventCallback OnCancel { get; set; } - - [Parameter] - public CompanyFormModel? InitialData { get; set; } - - private MudForm? form; + [Parameter, EditorRequired] public string ButtonText { get; set; } = "저장"; + [Parameter] public EventCallback OnSubmit { get; set; } + [Parameter] public EventCallback OnCancel { get; set; } + [Parameter] public CompanyFormModel? InitialData { get; set; } private CompanyFormModel model = new(); protected override void OnInitialized() @@ -63,17 +42,7 @@ } } - private async Task HandleSubmit() - { - if (form == null) - return; - - await form.Validate(); - if (!form.IsValid) - return; - - await OnSubmit.InvokeAsync(model); - } + private Task HandleSubmit() => OnSubmit.InvokeAsync(model); public class CompanyFormModel { diff --git a/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor b/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor index 4de00e4..25bb46f 100644 --- a/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor +++ b/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor @@ -1,61 +1,38 @@ @using TaxBaik.Application.DTOs @using TaxBaik.Application.Services +@using Microsoft.FluentUI.AspNetCore.Components - - +
+ + + + + 사업자세무 + 부동산세금 + 가족자산 + 기타 + + + + 신규 + 상담중 + 계약완료 + 거절 + 종결 + + - - - - - - 사업자세무 - 부동산세금 - 가족자산 - 기타 - - - - - - 신규 - 상담중 - 계약완료 - 거절 - 종결 - - - - -
- - @ButtonText - - 취소 +
+ +
- + @code { - [Parameter, EditorRequired] - public string ButtonText { get; set; } = "저장"; - - [Parameter] - public EventCallback OnSubmit { get; set; } - - [Parameter] - public EventCallback OnCancel { get; set; } - - [Parameter] - public InquiryFormModel? InitialData { get; set; } - - private MudForm? form; + [Parameter, EditorRequired] public string ButtonText { get; set; } = "저장"; + [Parameter] public EventCallback OnSubmit { get; set; } + [Parameter] public EventCallback OnCancel { get; set; } + [Parameter] public InquiryFormModel? InitialData { get; set; } private InquiryFormModel model = new(); protected override void OnInitialized() @@ -75,17 +52,7 @@ } } - private async Task HandleSubmit() - { - if (form == null) - return; - - await form.Validate(); - if (!form.IsValid) - return; - - await OnSubmit.InvokeAsync(model); - } + private Task HandleSubmit() => OnSubmit.InvokeAsync(model); public class InquiryFormModel { diff --git a/TaxBaik.Web/Components/Admin/InquiryTable.razor b/TaxBaik.Web/Components/Admin/InquiryTable.razor index edf43bb..316ce79 100644 --- a/TaxBaik.Web/Components/Admin/InquiryTable.razor +++ b/TaxBaik.Web/Components/Admin/InquiryTable.razor @@ -1,4 +1,5 @@ - +
+ @@ -18,22 +19,19 @@ } - +
이름@inquiry.Phone @inquiry.ServiceType - - @GetStatusLabel(inquiry.Status) - + @GetStatusLabel(inquiry.Status) @GetPreview(inquiry.Message) @inquiry.CreatedAt.ToString("yyyy-MM-dd") - 보기 - 수정 + 보기 + 수정
+
@code { [Parameter, EditorRequired] @@ -66,14 +64,14 @@ return trimmed.Length <= 30 ? trimmed : $"{trimmed[..30]}..."; } - private static Color GetStatusColor(string status) => status switch + private static string GetStatusClass(string status) => status switch { - "new" => Color.Warning, - "consulting" => Color.Info, - "contracted" => Color.Success, - "rejected" => Color.Error, - "closed" => Color.Dark, - _ => Color.Default + "new" => "warning", + "consulting" => "info", + "contracted" => "success", + "rejected" => "danger", + "closed" => "muted", + _ => "muted" }; private static string GetStatusLabel(string status) => InquiryStatusMapper.Labels.GetValueOrDefault(status, status); diff --git a/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor b/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor index ece20a4..7d6f217 100644 --- a/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor +++ b/TaxBaik.Web/Components/Admin/Layout/MainLayout.razor @@ -3,100 +3,86 @@ @inject IJSRuntime JS @implements IDisposable - - - +
+
+ +
- TaxBaik Admin - 세무회계 관리 대시보드 + TaxBaik Admin +

세무회계 관리 대시보드

- -
- - - 공개 사이트 - - - - - - - - 로그아웃 - - + + open_in_new + 공개 사이트 + + + logout + 로그아웃 +
- +
- + + +
+
@Body - - - +
+
+
@code { private bool drawerOpen = true; - private bool expandedCRMGroup = true; - private bool expandedCustomerGroup = false; - private bool expandedWebsiteGroup = false; protected override void OnInitialized() { @@ -113,15 +99,14 @@ StateHasChanged(); } + private string DrawerClass => drawerOpen ? "admin-drawer open" : "admin-drawer"; + private void OnLocationChanged(object? sender, LocationChangedEventArgs args) { _ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.showLoading")); } - private void ToggleDrawer() - { - drawerOpen = !drawerOpen; - } + private void ToggleDrawer() => drawerOpen = !drawerOpen; public void Dispose() { diff --git a/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementEdit.razor index 319dece..918da66 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementEdit.razor @@ -5,101 +5,47 @@ @using TaxBaik.Web.Services @inject IAnnouncementBrowserClient AnnouncementClient @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS @(Id.HasValue ? "공지 수정" : "공지 등록")
- Homepage - @(Id.HasValue ? "공지 수정" : "공지 등록") +
Homepage
+

@(Id.HasValue ? "공지 수정" : "공지 등록")

- - - - - - - - - - - - - - 일반 (파란색) - 배너 (주황색) — 중요 이벤트 - 긴급 (빨간색) — 마감 임박 - - - - - - - - - - - - - - - - - - - - -
- - @(isSaving ? "저장 중..." : "저장") - - - 취소 - +
+
+ + + + + + + +
+ +
- - +
+
@code { [Parameter] public int? Id { get; set; } - - private MudForm? form; private bool isSaving; private DateTime? startsAtDate; private DateTime? endsAtDate; - private AnnouncementDto model = new(); + private string StartsAtText { get => startsAtDate?.ToString("yyyy-MM-dd") ?? ""; set => startsAtDate = DateTime.TryParse(value, out var dt) ? dt : null; } + private string EndsAtText { get => endsAtDate?.ToString("yyyy-MM-dd") ?? ""; set => endsAtDate = DateTime.TryParse(value, out var dt) ? dt : null; } protected override async Task OnInitializedAsync() { @@ -115,15 +61,15 @@ } model = new AnnouncementDto { - Id = entity.Id, - Title = entity.Title, - Content = entity.Content, + Id = entity.Id, + Title = entity.Title, + Content = entity.Content, DisplayType = entity.DisplayType, - IsActive = entity.IsActive, - SortOrder = entity.SortOrder + IsActive = entity.IsActive, + SortOrder = entity.SortOrder }; startsAtDate = entity.StartsAt?.ToLocalTime(); - endsAtDate = entity.EndsAt?.ToLocalTime(); + endsAtDate = entity.EndsAt?.ToLocalTime(); } catch { @@ -134,41 +80,18 @@ private async Task SaveAsync() { - if (form is null) return; - await form.Validate(); - if (!form.IsValid) return; - isSaving = true; try { - model.StartsAt = startsAtDate.HasValue - ? DateTime.SpecifyKind(startsAtDate.Value.Date, DateTimeKind.Local).ToUniversalTime() - : null; - model.EndsAt = endsAtDate.HasValue - ? DateTime.SpecifyKind(endsAtDate.Value.Date.AddDays(1).AddSeconds(-1), DateTimeKind.Local).ToUniversalTime() - : null; - - if (Id.HasValue) - { - var result = await AnnouncementClient.UpdateAsync(Id.Value, model); - if (result != null) - Snackbar.Add("공지사항이 저장되었습니다.", Severity.Success); - else - Snackbar.Add("저장 실패", Severity.Error); - } - else - { - var result = await AnnouncementClient.CreateAsync(model); - if (result != null) - Snackbar.Add("공지사항이 저장되었습니다.", Severity.Success); - else - Snackbar.Add("저장 실패", Severity.Error); - } + model.StartsAt = startsAtDate.HasValue ? DateTime.SpecifyKind(startsAtDate.Value.Date, DateTimeKind.Local).ToUniversalTime() : null; + model.EndsAt = endsAtDate.HasValue ? DateTime.SpecifyKind(endsAtDate.Value.Date.AddDays(1).AddSeconds(-1), DateTimeKind.Local).ToUniversalTime() : null; + var result = Id.HasValue ? await AnnouncementClient.UpdateAsync(Id.Value, model) : await AnnouncementClient.CreateAsync(model); + await JS.InvokeVoidAsync("alert", result != null ? "공지사항이 저장되었습니다." : "저장 실패"); Navigation.NavigateTo("/taxbaik/admin/announcements"); } catch (Exception ex) { - Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}"); } finally { diff --git a/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementList.razor b/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementList.razor index df47db0..c64d12d 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Announcements/AnnouncementList.razor @@ -4,90 +4,77 @@ @using TaxBaik.Domain.Entities @inject IAnnouncementBrowserClient AnnouncementClient @inject NavigationManager Navigation -@inject IDialogService DialogService -@inject ISnackbar Snackbar +@inject IJSRuntime JS 공지사항 관리
- Homepage - 공지사항 관리 - 홈페이지 상단에 노출되는 공지사항을 등록하고 관리합니다. +
Homepage
+

공지사항 관리

+

홈페이지 상단에 노출되는 공지사항을 등록하고 관리합니다.

- - 공지 등록 - + 공지 등록
- +
@if (announcements is null) { - + } else if (!announcements.Any()) { - 등록된 공지사항이 없습니다. +
등록된 공지사항이 없습니다.
} else { - - - - 제목 - 유형 - 상태 - 게시 기간 - 순서 - - - - - @foreach (var item in announcements) - { +
+ + - - - - - - + + + + + + - } - - + + + @foreach (var item in announcements) + { + + + + + + + + + } + +
@item.Title - - @GetTypeLabel(item.DisplayType) - - - @if (IsCurrentlyActive(item)) - { - 노출 중 - } - else if (!item.IsActive) - { - 비활성 - } - else - { - 기간 외 - } - - @FormatPeriod(item) - @item.SortOrder - - - 수정 - - - 삭제 - - - 제목유형상태게시 기간순서
@item.Title@GetTypeLabel(item.DisplayType) + @if (IsCurrentlyActive(item)) + { + 노출 중 + } + else if (!item.IsActive) + { + 비활성 + } + else + { + 기간 외 + } + @FormatPeriod(item)@item.SortOrder +
+ + +
+
+
} - +
@code { [CascadingParameter] @@ -97,16 +84,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) - { - await LoadAsync(); - StateHasChanged(); - } + await LoadAsync(); + StateHasChanged(); } } } @@ -119,36 +103,32 @@ } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); announcements = []; } } private async Task DeleteAsync(Announcement item) { - var confirmed = await DialogService.ShowMessageBox( - "공지 삭제", - $"'{item.Title}' 공지를 삭제하시겠습니까?", - yesText: "삭제", cancelText: "취소"); - - if (confirmed != true) return; + var confirmed = await JS.InvokeAsync("confirm", $"'{item.Title}' 공지를 삭제하시겠습니까?"); + if (!confirmed) return; try { var success = await AnnouncementClient.DeleteAsync(item.Id); if (success) { - Snackbar.Add("공지사항이 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "공지사항이 삭제되었습니다."); await LoadAsync(); } else { - Snackbar.Add("삭제 실패", Severity.Error); + await JS.InvokeVoidAsync("alert", "삭제 실패"); } } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); } } @@ -157,28 +137,21 @@ if (!a.IsActive) return false; var now = DateTime.UtcNow; if (a.StartsAt.HasValue && a.StartsAt > now) return false; - if (a.EndsAt.HasValue && a.EndsAt < now) return false; + if (a.EndsAt.HasValue && a.EndsAt < now) return false; return true; } private static string FormatPeriod(Announcement a) { var start = a.StartsAt?.ToLocalTime().ToString("MM/dd") ?? "즉시"; - var end = a.EndsAt?.ToLocalTime().ToString("MM/dd") ?? "무기한"; + var end = a.EndsAt?.ToLocalTime().ToString("MM/dd") ?? "무기한"; return $"{start} ~ {end}"; } - private static Color GetTypeColor(string type) => type switch - { - "urgent" => Color.Error, - "banner" => Color.Warning, - _ => Color.Info - }; - private static string GetTypeLabel(string type) => type switch { "urgent" => "긴급", "banner" => "배너", - _ => "일반" + _ => "일반" }; } diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor index 5999b08..aa2526e 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogCreate.razor @@ -6,77 +6,53 @@ @inject BlogService BlogService @inject ICategoryRepository CategoryRepository @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS 새 포스트 작성 -
- Content - 새 포스트 작성 - 새로운 블로그 포스트를 작성합니다. +
Content
+

새 포스트 작성

+

새로운 블로그 포스트를 작성합니다.

- 취소 +
- - - - - - @foreach (var category in categories) - { - @category.Name - } - - - - - - - - - - - - -
- 저장 +
+
+ + + + + + + +
+
- - +
+
@code { - private MudForm? form; private List categories = []; private CreatePostModel model = new(); + private string CategoryIdText { get => model.CategoryId?.ToString() ?? ""; set => model.CategoryId = int.TryParse(value, out var id) ? id : null; } protected override async Task OnInitializedAsync() { categories = (await CategoryRepository.GetAllAsync()).ToList(); } - private void GoBack() - { - Navigation.NavigateTo("/taxbaik/admin/blog"); - } - private async Task SavePost() { - if (form == null) - return; - - await form.Validate(); - if (!form.IsValid) - return; - try { await BlogService.CreateAsync(new CreateBlogPostDto @@ -90,12 +66,12 @@ IsPublished = model.IsPublished }); - Snackbar.Add("포스트가 저장되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "포스트가 저장되었습니다."); Navigation.NavigateTo("/taxbaik/admin/blog"); } catch (ValidationException ex) { - Snackbar.Add(ex.Message, Severity.Error); + await JS.InvokeVoidAsync("alert", ex.Message); } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor index de0d296..65841bd 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogEdit.razor @@ -6,76 +6,60 @@ @inject BlogService BlogService @inject ICategoryRepository CategoryRepository @inject NavigationManager Navigation -@inject ISnackbar Snackbar -@inject IDialogService DialogService +@inject IJSRuntime JS 포스트 수정 -
- Content - 포스트 수정 - 블로그 포스트를 수정합니다. +
Content
+

포스트 수정

+

블로그 포스트를 수정합니다.

- 취소 +
@if (isLoading) { - +
} else if (post == null) { - 포스트를 찾을 수 없습니다. +
포스트를 찾을 수 없습니다.
} else { - - - - - - @foreach (var category in categories) - { - @category.Name - } - - - - - - - - - - - - -
- 저장 - 삭제 +
+
+ + + + + + + +
+ +
- - +
+
} @code { - [Parameter] - public int Id { get; set; } - - private MudForm? form; + [Parameter] public int Id { get; set; } private Domain.Entities.BlogPost? post; private List categories = []; private EditPostModel model = new(); private bool isLoading = true; + private string CategoryIdText { get => model.CategoryId?.ToString() ?? ""; set => model.CategoryId = int.TryParse(value, out var id) ? id : null; } protected override async Task OnInitializedAsync() { @@ -90,7 +74,7 @@ else } catch (Exception ex) { - Snackbar.Add($"포스트 로드 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"포스트 로드 실패: {ex.Message}"); } finally { @@ -109,20 +93,9 @@ else model.IsPublished = post.IsPublished; } - private void GoBack() - { - Navigation.NavigateTo("/taxbaik/admin/blog"); - } - private async Task SavePost() { - if (form == null || post == null) - return; - - await form.Validate(); - if (!form.IsValid) - return; - + if (post == null) return; try { await BlogService.UpdateAsync(post.Id, new CreateBlogPostDto @@ -135,43 +108,22 @@ else SeoDescription = model.SeoDescription, IsPublished = model.IsPublished }); - - Snackbar.Add("포스트가 저장되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "포스트가 저장되었습니다."); Navigation.NavigateTo("/taxbaik/admin/blog"); } catch (ValidationException ex) { - Snackbar.Add(ex.Message, Severity.Error); - } - catch (Exception ex) - { - Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", ex.Message); } } private async Task DeletePost() { - if (post == null) - return; - - var result = await DialogService.ShowMessageBox( - "포스트 삭제", - "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", - "삭제", "취소"); - - if (result != true) - return; - - try - { - await BlogService.DeleteAsync(post.Id); - Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success); - Navigation.NavigateTo("/taxbaik/admin/blog"); - } - catch (Exception ex) - { - Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); - } + if (post == null) return; + if (!await JS.InvokeAsync("confirm", "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.")) return; + await BlogService.DeleteAsync(post.Id); + await JS.InvokeVoidAsync("alert", "포스트가 삭제되었습니다."); + Navigation.NavigateTo("/taxbaik/admin/blog"); } private class EditPostModel diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor index d8d982d..a7086b8 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor @@ -1,58 +1,72 @@ @page "/admin/blog" @attribute [Authorize] @inject IApiClient ApiClient -@inject ISnackbar Snackbar +@inject IJSRuntime JS 블로그 관리 -
- Content - 블로그 관리 - 검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다. +
Content
+

블로그 관리

+

검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.

- 새 포스트 작성 +
- - - @($"전체 포스트 {totalPosts}개") - 페이지 @currentPage / @totalPages - - +
+
+ 전체 포스트: @($"{totalPosts}개") + 페이지 @currentPage / @totalPages +
+
- - - - - - - - - - - - - 수정하기 - 삭제 - - - - +
+ @if (isLoading) + { + + } + else + { +
+ + + + + + + + + + + + @foreach (var post in posts) + { + + + + + + + + } + +
제목발행조회수작성일
@post.Title@post.ViewCount@post.CreatedAt.ToString("yyyy-MM-dd") +
+ 수정 + +
+
+
+ } +
- - 이전 - 다음 - +
+ + +
@code { - [CascadingParameter] - private Task? AuthStateTask { get; set; } - + [CascadingParameter] private Task? AuthStateTask { get; set; } private List posts = []; private bool isLoading = true; private int currentPage = 1; @@ -62,20 +76,19 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) - { - await LoadPosts(); - StateHasChanged(); - } + await LoadPosts(); + StateHasChanged(); } } } + private string NavTo(string url) => url; + private async Task LoadPosts() { isLoading = true; @@ -92,58 +105,33 @@ totalPosts = 0; totalPages = 1; } - isLoading = false; + finally + { + isLoading = false; + } } - private async Task PreviousPage() - { - if (currentPage <= 1) - return; - - currentPage--; - await LoadPosts(); - } - - private async Task NextPage() - { - if (currentPage >= totalPages) - return; - - currentPage++; - await LoadPosts(); - } + private async Task PreviousPage() { if (currentPage > 1) { currentPage--; await LoadPosts(); } } + private async Task NextPage() { if (currentPage < totalPages) { currentPage++; await LoadPosts(); } } private async Task TogglePublish(TaxBaik.Domain.Entities.BlogPost post, bool isPublished) { var previous = post.IsPublished; post.IsPublished = isPublished; - var result = await ApiClient.PutAsync($"blog/{post.Id}", new - { - post.Title, - post.Content, - post.CategoryId, - post.Tags, - post.SeoTitle, - post.SeoDescription, - post.ThumbnailUrl, - IsPublished = isPublished, - post.AuthorId - }); - + var result = await ApiClient.PutAsync($"blog/{post.Id}", new { post.Title, post.Content, post.CategoryId, post.Tags, post.SeoTitle, post.SeoDescription, post.ThumbnailUrl, IsPublished = isPublished, post.AuthorId }); if (result == null) { post.IsPublished = previous; - Snackbar.Add("발행 상태 변경에 실패했습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", "발행 상태 변경에 실패했습니다."); return; } - - Snackbar.Add("발행 상태가 변경되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "발행 상태가 변경되었습니다."); } private async Task DeletePost(int postId) { await ApiClient.DeleteAsync($"blog/{postId}"); - Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "포스트가 삭제되었습니다."); await LoadPosts(); } diff --git a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientDetail.razor b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientDetail.razor index 2fe9c75..b45542c 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientDetail.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientDetail.razor @@ -4,185 +4,123 @@ @inject ClientService ClientService @inject ConsultationService ConsultationService @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS 고객 상세 -
- Client Details - 고객 상세 - 고객 정보와 상담 이력을 관리합니다. +
Client Details
+

고객 상세

+

고객 정보와 상담 이력을 관리합니다.

@if (client == null) { - 고객을 찾을 수 없습니다. - return; +
고객을 찾을 수 없습니다.
} +else +{ +
+ + 수정 +
- - - 목록으로 - - - 수정 - - - - - - - 고객 정보 - - - 이름 - @client.Name - - - 상호 - @(client.CompanyName ?? "-") - - - 연락처 - @(client.Phone ?? "-") - - - 이메일 - @(client.Email ?? "-") - - - 서비스 - @(client.ServiceType ?? "-") - - - 사업자 유형 - @(client.TaxType ?? "-") - - - 유입 경로 - @(client.Source ?? "-") - - - 등록일 - @client.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd") - +
+
+

고객 정보

+
+
이름@client.Name
+
상호@(client.CompanyName ?? "-")
+
연락처@(client.Phone ?? "-")
+
이메일@(client.Email ?? "-")
+
서비스@(client.ServiceType ?? "-")
+
사업자 유형@(client.TaxType ?? "-")
+
유입 경로@(client.Source ?? "-")
+
등록일@client.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")
@if (!string.IsNullOrWhiteSpace(client.Memo)) { - - 메모 - @client.Memo - +
메모@client.Memo
} - - - +
+
- - - - 상담 이력 - - + 상담 추가 - - +
+
+
+

상담 이력

+
+ +
@if (showAddForm) { - - - - - - - - @foreach (var t in ClientService.ServiceTypes) - { - @t - } - - - - - - - - - - @foreach (var r in ConsultationService.Results) - { - @r - } - - - - - - - - 저장 - 취소 - - +
+ + + + + +
+ + +
+
} @if (consultations.Count == 0) { - 상담 이력이 없습니다. +

상담 이력이 없습니다.

} else { - +
@foreach (var c in consultations) { - - - -
- - @c.ConsultationDate.ToString("yyyy-MM-dd") - @if (!string.IsNullOrEmpty(c.ServiceType)) { · @c.ServiceType } - - @c.Summary - @if (!string.IsNullOrEmpty(c.Result)) - { - @c.Result - } - @if (c.Fee.HasValue) - { - - 수임료: @c.Fee.Value.ToString("N0")원 - - } -
- -
-
-
+
+
+
+ @c.ConsultationDate.ToString("yyyy-MM-dd") @(string.IsNullOrEmpty(c.ServiceType) ? "" : $"· {c.ServiceType}") +
+ +
+

@c.Summary

+ @if (!string.IsNullOrEmpty(c.Result)) + { + @c.Result + } + @if (c.Fee.HasValue) + { +
수임료: @c.Fee.Value.ToString("N0")원
+ } +
} - +
} - - - +
+
+} @code { - [Parameter] - public int ClientId { get; set; } - + [Parameter] public int ClientId { get; set; } private Domain.Entities.Client? client; private List consultations = []; - private bool showAddForm; private DateTime? newDate = DateTime.Today; private string newServiceType = ""; @@ -190,10 +128,10 @@ private string newResult = ""; private decimal? newFee; - protected override async Task OnInitializedAsync() - { - await LoadAll(); - } + private string ConsultationDateText { get => newDate?.ToString("yyyy-MM-dd") ?? ""; set => newDate = DateTime.TryParse(value, out var dt) ? dt : null; } + private string FeeText { get => newFee?.ToString() ?? ""; set => newFee = decimal.TryParse(value, out var d) ? d : null; } + + protected override async Task OnInitializedAsync() => await LoadAll(); private async Task LoadAll() { @@ -215,6 +153,12 @@ { try { + if (string.IsNullOrWhiteSpace(newSummary)) + { + await JS.InvokeVoidAsync("alert", "상담 내용을 입력하세요."); + return; + } + var c = new Domain.Entities.Consultation { ClientId = ClientId, @@ -224,21 +168,23 @@ Result = string.IsNullOrWhiteSpace(newResult) ? null : newResult, Fee = newFee }; + await ConsultationService.CreateAsync(c); showAddForm = false; consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList(); - Snackbar.Add("상담이 추가되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "상담이 추가되었습니다."); } catch (ValidationException ex) { - Snackbar.Add(ex.Message, Severity.Error); + await JS.InvokeVoidAsync("alert", ex.Message); } } private async Task DeleteConsultation(int id) { + if (!await JS.InvokeAsync("confirm", "이 상담을 삭제하시겠습니까?")) return; await ConsultationService.DeleteAsync(id); consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList(); - Snackbar.Add("삭제되었습니다.", Severity.Info); + await JS.InvokeVoidAsync("alert", "삭제되었습니다."); } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor index c31862e..8966d13 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor @@ -6,117 +6,74 @@ @using TaxBaik.Domain.Entities @inject IClientBrowserClient ClientClient @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS @(Id.HasValue ? "고객 수정" : "고객 등록") -
- CRM - @(Id.HasValue ? "고객 수정" : "고객 등록") +
CRM
+

@(Id.HasValue ? "고객 수정" : "고객 등록")

- 목록으로 +
- +
@if (isLoading) { - + } else { - - - @* 기본 정보 *@ - - 기본 정보 - - - - - - - - - - - - - - - - @* 세무 정보 *@ - - 세무 정보 - - - - - @foreach (var t in ClientService.ServiceTypes) - { - @t - } - - - - - @foreach (var t in ClientService.TaxTypes) - { - @t - } - - - - @* 관리 정보 *@ - - 관리 정보 - - - - - 활성 - 비활성 - - - - - @foreach (var s in ClientService.Sources) - { - @s - } - - - - - - - @* 저장 버튼 *@ - - - @(isSaving ? "저장 중..." : "저장") - - - 취소 - - - - +
+ + + + + + + + + +
+ + +
+
} - +
@code { [Parameter] public int? Id { get; set; } - - private MudForm form = null!; private CreateClientDto dto = new() { Status = "active" }; - private bool isValid; private bool isLoading = true; private bool isSaving; @@ -129,7 +86,7 @@ var client = await ClientClient.GetByIdAsync(Id.Value); if (client is null) { - Snackbar.Add("고객을 찾을 수 없습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", "고객을 찾을 수 없습니다."); Navigation.NavigateTo("/taxbaik/admin/clients"); return; } @@ -145,46 +102,42 @@ Source = client.Source, Memo = client.Memo }; - } - catch (Exception ex) - { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); - Navigation.NavigateTo("/taxbaik/admin/clients"); - return; - } + } + catch (Exception ex) + { + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); + Navigation.NavigateTo("/taxbaik/admin/clients"); + return; + } } isLoading = false; } private async Task SaveAsync() { - await form.Validate(); - if (!isValid) return; - isSaving = true; try { + if (string.IsNullOrWhiteSpace(dto.Name)) + { + await JS.InvokeVoidAsync("alert", "고객명을 입력하세요."); + return; + } if (Id.HasValue) { var result = await ClientClient.UpdateAsync(Id.Value, dto); - if (result != null) - Snackbar.Add("고객 정보가 수정되었습니다.", Severity.Success); - else - Snackbar.Add("수정에 실패했습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", result != null ? "고객 정보가 수정되었습니다." : "수정에 실패했습니다."); } else { var result = await ClientClient.CreateAsync(dto); - if (result != null) - Snackbar.Add("고객이 등록되었습니다.", Severity.Success); - else - Snackbar.Add("등록에 실패했습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", result != null ? "고객이 등록되었습니다." : "등록에 실패했습니다."); } Navigation.NavigateTo("/taxbaik/admin/clients"); } catch (Exception ex) { - Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}"); } finally { diff --git a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor index 790e06a..e0b14a0 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor @@ -4,134 +4,94 @@ @using TaxBaik.Domain.Entities @inject IClientBrowserClient ClientClient @inject NavigationManager Navigation -@inject IDialogService DialogService -@inject ISnackbar Snackbar +@inject IJSRuntime JS 고객 관리 -
- CRM - 고객 관리 - 고객 카드를 등록하고 상담 이력을 관리합니다. +
CRM
+

고객 관리

+

고객 카드를 등록하고 상담 이력을 관리합니다.

- - 고객 등록 - +
-@* 검색/필터 바 *@ - - - - - - - - 전체 - 활성 - 비활성 - - - - 검색 - - - 초기화 - - - +
+
+ + + + +
+
- +
@if (clients is null) { - + } else if (!clients.Any()) { -
- - 등록된 고객이 없습니다. -
+
등록된 고객이 없습니다.
} else { - - - - 이름 - 회사명 - 연락처 - 서비스 - 세금 유형 - 상태 - 유입 경로 - 등록일 - - - - - @foreach (var c in clients) - { +
+ + - - - - - - - - - + + + + + + + + + - } - - - - @* 페이징 *@ + + + @foreach (var c in clients) + { + + + + + + + + + + + + } + +
@c.Name@(c.CompanyName ?? "—")@(c.Phone ?? "—") - @if (!string.IsNullOrEmpty(c.ServiceType)) - { - @c.ServiceType - } - @(c.TaxType ?? "—") - @if (c.Status == "active") - { - 활성 - } - else - { - 비활성 - } - @(c.Source ?? "—")@c.CreatedAt.ToLocalTime().ToString("yy.MM.dd") - - - 수정 - - - 삭제 - - - 이름회사명연락처서비스세금 유형상태유입 경로등록일
@c.Name@(c.CompanyName ?? "—")@(c.Phone ?? "—")@(c.ServiceType ?? "—")@(c.TaxType ?? "—")@(c.Status == "active" ? "활성" : "비활성")@(c.Source ?? "—")@c.CreatedAt.ToLocalTime().ToString("yy.MM.dd") +
+ + +
+
+
@if (totalPages > 1) { -
- +
+ + @currentPage / @totalPages +
} - 총 @(totalCount)명 + } - +
@code { - [CascadingParameter] - private Task? AuthStateTask { get; set; } - + [CascadingParameter] private Task? AuthStateTask { get; set; } private List? clients; private string searchText = ""; private string statusFilter = ""; @@ -142,16 +102,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) - { - await LoadAsync(); - StateHasChanged(); - } + await LoadAsync(); + StateHasChanged(); } } } @@ -160,75 +117,39 @@ { try { - var (items, total) = await ClientClient.GetPagedAsync( - currentPage, PageSize, - string.IsNullOrEmpty(statusFilter) ? null : statusFilter, - string.IsNullOrEmpty(searchText) ? null : searchText); - + var (items, total) = await ClientClient.GetPagedAsync(currentPage, PageSize, string.IsNullOrEmpty(statusFilter) ? null : statusFilter, string.IsNullOrEmpty(searchText) ? null : searchText); clients = items.ToList(); totalCount = total; totalPages = (int)Math.Ceiling((double)total / PageSize); } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); clients = []; - totalCount = 0; - totalPages = 0; } } - private async Task SearchAsync() - { - currentPage = 1; - await LoadAsync(); - } - - private async Task ResetAsync() - { - searchText = ""; - statusFilter = ""; - currentPage = 1; - await LoadAsync(); - } - - private async Task OnPageChanged(int page) - { - currentPage = page; - await LoadAsync(); - } - - private async Task OnSearchKeyUp(KeyboardEventArgs e) - { - if (e.Key == "Enter") await SearchAsync(); - } - + private async Task SearchAsync() { currentPage = 1; await LoadAsync(); } + private async Task ResetAsync() { searchText = ""; statusFilter = ""; currentPage = 1; await LoadAsync(); } + private async Task PreviousPage() { if (currentPage > 1) { currentPage--; await LoadAsync(); } } + private async Task NextPage() { if (currentPage < totalPages) { currentPage++; await LoadAsync(); } } + private async Task OnSearchKeyUp(KeyboardEventArgs e) { if (e.Key == "Enter") await SearchAsync(); } private async Task DeleteAsync(Client client) { - var confirmed = await DialogService.ShowMessageBox( - "고객 삭제", - $"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다.", - yesText: "삭제", cancelText: "취소"); - - if (confirmed != true) return; - + var confirmed = await JS.InvokeAsync("confirm", $"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다."); + if (!confirmed) return; try { var success = await ClientClient.DeleteAsync(client.Id); if (success) { - Snackbar.Add($"{client.Name} 고객이 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", $"{client.Name} 고객이 삭제되었습니다."); await LoadAsync(); } - else - { - Snackbar.Add("삭제에 실패했습니다.", Severity.Error); - } } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); } - await LoadAsync(); } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor index 2534826..8e515a6 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor @@ -3,22 +3,22 @@ @using TaxBaik.Web.Components.Admin.Forms @inject IApiClient ApiClient @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS 고객사 등록
- Settings - 새 고객사 등록 - 새로운 고객사를 추가합니다. +
Settings
+

새 고객사 등록

+

새로운 고객사를 추가합니다.

- 취소 +
- +
- +
@code { private void GoBack() @@ -40,12 +40,12 @@ memo = model.Memo }); - Snackbar.Add("고객사가 등록되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "고객사가 등록되었습니다."); Navigation.NavigateTo("/taxbaik/admin/companies"); } catch (Exception ex) { - Snackbar.Add($"등록 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"등록 실패: {ex.Message}"); } } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor index deb247e..a5793c9 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor @@ -3,39 +3,37 @@ @using TaxBaik.Web.Components.Admin.Forms @inject IApiClient ApiClient @inject NavigationManager Navigation -@inject ISnackbar Snackbar -@inject IDialogService DialogService +@inject IJSRuntime JS 고객사 수정
- Settings - 고객사 수정 - 고객사 정보를 수정합니다. +
Settings
+

고객사 수정

+

고객사 정보를 수정합니다.

- 취소 +
@if (isLoading) { - +
+ +
} else if (formModel == null) { - 고객사를 찾을 수 없습니다. +
고객사를 찾을 수 없습니다.
} else { - +
- - - - - 고객사 삭제 - - +
+ +
+
} @code { @@ -67,7 +65,7 @@ else } catch (Exception ex) { - Snackbar.Add($"고객사 로드 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"고객사 로드 실패: {ex.Message}"); } finally { @@ -95,34 +93,29 @@ else isActive = model.IsActive }); - Snackbar.Add("고객사가 수정되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "고객사가 수정되었습니다."); Navigation.NavigateTo("/taxbaik/admin/companies"); } catch (Exception ex) { - Snackbar.Add($"수정 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"수정 실패: {ex.Message}"); } } private async Task DeleteCompany() { - var result = await DialogService.ShowMessageBox( - "고객사 삭제", - "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", - "삭제", "취소"); - - if (result != true) + if (!await JS.InvokeAsync("confirm", "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.")) return; try { await ApiClient.DeleteAsync($"company/{Id}"); - Snackbar.Add("고객사가 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "고객사가 삭제되었습니다."); Navigation.NavigateTo("/taxbaik/admin/companies"); } catch (Exception ex) { - Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}"); } } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor index b0cd304..5156b9e 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor @@ -1,53 +1,71 @@ @page "/admin/companies" @attribute [Authorize] @inject IApiClient ApiClient -@inject ISnackbar Snackbar +@inject IJSRuntime JS 고객사 관리
- Settings - 고객사 관리 - 등록된 고객사를 관리하고 새로운 고객사를 추가합니다. +
Settings
+

고객사 관리

+

등록된 고객사를 관리하고 새로운 고객사를 추가합니다.

- 새 고객사 등록 +
- - - @($"전체 고객사 {totalCompanies}개") - 페이지 @currentPage / @totalPages - - +
+
+ @($"전체 고객사 {totalCompanies}개") + 페이지 @currentPage / @totalPages +
+
- - - - - - - - - - - - - - - - 수정 - - - - +
+ @if (isLoading) + { + + } + else + { +
+ + + + + + + + + + + + + + + @foreach (var item in companies) + { + + + + + + + + + + + } + +
회사코드회사명담당자전화이메일활성등록일
@item.CompanyCode@item.CompanyName@(item.ContactPerson ?? "—")@(item.Phone ?? "—")@(item.Email ?? "—")@(item.IsActive ? "활성" : "비활성")@item.CreatedAt.ToString("yyyy-MM-dd")수정
+
+ } +
- - 이전 - 다음 - +
+ + +
@code { private List companies = []; @@ -100,7 +118,7 @@ } catch (Exception ex) { - Snackbar.Add($"고객사 로드 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"고객사 로드 실패: {ex.Message}"); } finally { @@ -131,4 +149,6 @@ public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } } + + private string NavTo(string url) => url; } diff --git a/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor b/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor index bb5cb62..0095e82 100644 --- a/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor +++ b/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor @@ -2,150 +2,122 @@ @using TaxBaik.Web.Services.AdminClients @inject IConsultingActivityBrowserClient ActivityClient @inject IClientBrowserClient ClientClient -@inject ISnackbar Snackbar -@inject IDialogService DialogService +@inject IJSRuntime JS @attribute [Authorize] 상담 활동 관리 -
- CRM & 세무관리 - 상담 활동 관리 - 고객별 상담 이력과 팔로업을 추적합니다. +
CRM & 세무관리
+

상담 활동 관리

+

고객별 상담 이력과 팔로업을 추적합니다.

- - 새 활동 기록 - +
- +
@if (activities is 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") - - } - - - - - - - - - - - - +
+ + + + + + + + + + + + + + @foreach (var item in activities) + { + + + + + + + + + + } + +
ID고객활동 유형활동일시설명다음 팔로업작업
@item.Id@clientMap.GetValueOrDefault(item.ClientId, $"Client #{item.ClientId}")@item.ActivityType@item.ActivityDate.ToString("g")@Truncate(item.Description)@(item.NextFollowupDate?.ToString("yyyy-MM-dd") ?? "—") +
+ + +
+
+
} - +
- - - @(editingActivity == null ? "새 활동 기록" : "활동 기록 수정") - - - - + +
+

@(editingActivity == null ? "새 활동 기록" : "활동 기록 수정")

+ + + + + +
+ + +
+
+
@code { - [CascadingParameter] - private Task? AuthStateTask { get; set; } - + [CascadingParameter] private Task? AuthStateTask { get; set; } private List? activities; private List clients = []; private Dictionary clientMap = new(); - private MudForm? form; private bool isDialogOpen; private ConsultingActivity? editingActivity; private ConsultingActivityForm activityForm = new(); + private string ClientIdText { get => activityForm.ClientId > 0 ? activityForm.ClientId.ToString() : ""; set => activityForm.ClientId = int.TryParse(value, out var id) ? id : 0; } + private string ActivityDateText { get => activityForm.ActivityDate?.ToString("yyyy-MM-dd HH:mm") ?? ""; set => activityForm.ActivityDate = DateTime.TryParse(value, out var dt) ? dt : null; } + private string NextFollowupText { get => activityForm.NextFollowupDate?.ToString("yyyy-MM-dd") ?? ""; set => activityForm.NextFollowupDate = DateTime.TryParse(value, out var dt) ? dt : null; } + protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) - { - await LoadData(); - StateHasChanged(); - } + await LoadData(); + StateHasChanged(); } } } @@ -161,18 +133,14 @@ } catch (Exception ex) { - Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"데이터 로드 실패: {ex.Message}"); } } private void OpenCreateDialog() { editingActivity = null; - activityForm = new ConsultingActivityForm - { - ActivityDate = DateTime.Now, - ClientId = clients.FirstOrDefault()?.Id ?? 0 - }; + activityForm = new ConsultingActivityForm { ClientId = clients.FirstOrDefault()?.Id ?? 0, ActivityDate = DateTime.Now }; isDialogOpen = true; } @@ -188,103 +156,60 @@ NextFollowupDate = activity.NextFollowupDate }; isDialogOpen = true; + await Task.CompletedTask; } private async Task SaveActivity() { - if (form != null) + if (activityForm.ClientId <= 0 || string.IsNullOrWhiteSpace(activityForm.ActivityType) || string.IsNullOrWhiteSpace(activityForm.Description)) { - await form.Validate(); - if (!form.IsValid) - { - Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning); - return; - } + await JS.InvokeVoidAsync("alert", "필수 항목을 입력해주세요."); + 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); - + var newId = await ActivityClient.CreateAsync(activityForm.ClientId, activityForm.ActivityType, activityForm.ActivityDate ?? DateTime.Now, activityForm.Description, null, activityForm.NextFollowupDate); if (newId > 0) { - Snackbar.Add("활동이 기록되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "활동이 기록되었습니다."); CloseDialog(); await LoadData(); } } else { - await ActivityClient.UpdateAsync( - editingActivity.Id, - null, - activityForm.NextFollowupDate); - - Snackbar.Add("활동이 업데이트되었습니다.", Severity.Success); + await ActivityClient.UpdateAsync(editingActivity.Id, null, activityForm.NextFollowupDate); + await JS.InvokeVoidAsync("alert", "활동이 업데이트되었습니다."); CloseDialog(); await LoadData(); } } catch (Exception ex) { - Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}"); } } private async Task DeleteActivity(int id) { - var parameters = new DialogParameters - { - { "Title", "삭제 확인" }, - { "Message", "이 활동을 삭제하시겠습니까?" } - }; - - var dialog = await DialogService.ShowAsync("", parameters); - var result = await dialog.Result; - - if (result?.Canceled ?? true) - return; - + if (!await JS.InvokeAsync("confirm", "이 활동을 삭제하시겠습니까?")) return; try { await ActivityClient.DeleteAsync(id); - Snackbar.Add("활동이 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "활동이 삭제되었습니다."); await LoadData(); } catch (Exception ex) { - Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}"); } } - 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; } - } + private void CloseDialog() { isDialogOpen = false; editingActivity = null; activityForm = new(); } + private static string Truncate(string? text) => string.IsNullOrWhiteSpace(text) ? "—" : text.Length > 30 ? text[..30] + "..." : text; + private static string GetClientDisplayName(Client client) => !string.IsNullOrWhiteSpace(client.CompanyName) ? client.CompanyName : !string.IsNullOrWhiteSpace(client.Name) ? client.Name : $"Client #{client.Id}"; + private sealed 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 index 9d480be..c924ff0 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Contracts.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor @@ -2,160 +2,123 @@ @using TaxBaik.Web.Services.AdminClients @inject IContractBrowserClient ContractClient @inject IClientBrowserClient ClientClient -@inject ISnackbar Snackbar -@inject IDialogService DialogService +@inject IJSRuntime JS @attribute [Authorize] 계약 관리
- CRM & 세무관리 - 계약 관리 - 고객 계약과 월 정기수익을 함께 관리합니다. +
CRM & 세무관리
+

계약 관리

+

고객 계약과 월 정기수익을 함께 관리합니다.

@if (mrr > 0) { - - 월 정기수익: - ₩@mrr.ToString("N0") - +

월 정기수익: ₩@mrr.ToString("N0")

}
- - 새 계약 추가 - +
- +
@if (contracts is 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 item in contracts) + { + var isActive = !item.EndDate.HasValue || item.EndDate.Value >= DateTime.Today; + + + + + + + + + + + } + +
ID고객계약번호서비스 유형월 수수료계약기간상태작업
@item.Id@(clientMap.TryGetValue(item.ClientId, out var clientName) ? clientName : "")@item.ContractNumber@item.ServiceType@(item.MonthlyFee?.ToString("C") ?? "—")@item.StartDate@if (item.EndDate.HasValue){~ @item.EndDate.Value}@(isActive ? "활성" : "만료")
+
} - +
- - - - 새 계약 추가 - - - - + +
+

새 계약 추가

+ + + + + +
+ + +
+
+
@code { - [CascadingParameter] - private Task? AuthStateTask { get; set; } - + [CascadingParameter] private Task? AuthStateTask { get; set; } private List? contracts; private List clients = []; private Dictionary clientMap = new(); private decimal mrr = 0; - private MudForm? form; private bool isDialogOpen; private ContractForm contractForm = new(); + private string ClientIdText { get => contractForm.ClientId > 0 ? contractForm.ClientId.ToString() : ""; set => contractForm.ClientId = int.TryParse(value, out var id) ? id : 0; } + private string StartDateText { get => contractForm.StartDate?.ToString("yyyy-MM-dd") ?? ""; set => contractForm.StartDate = DateTime.TryParse(value, out var dt) ? dt : null; } + private string MonthlyFeeText { get => contractForm.MonthlyFee?.ToString() ?? ""; set => contractForm.MonthlyFee = decimal.TryParse(value, out var amount) ? amount : null; } protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) - { - await LoadData(); - StateHasChanged(); - } + await LoadData(); + StateHasChanged(); } } } @@ -172,99 +135,56 @@ } catch (Exception ex) { - Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"데이터 로드 실패: {ex.Message}"); } } private void OpenCreateDialog() { - contractForm = new ContractForm - { - ClientId = clients.FirstOrDefault()?.Id, - StartDate = DateTime.Today - }; + contractForm = new ContractForm { ClientId = clients.FirstOrDefault()?.Id ?? 0, StartDate = DateTime.Today }; isDialogOpen = true; } private async Task SaveContract() { - if (form != null) - { - await form.Validate(); - if (!form.IsValid) - { - Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning); - return; - } - } - try { - if (contractForm.ClientId == null) return; - var newId = await ContractClient.CreateAsync( - contractForm.ClientId.Value, - contractForm.ContractNumber, - contractForm.ServiceType, - contractForm.StartDate ?? DateTime.Now, - contractForm.MonthlyFee); + if (contractForm.ClientId <= 0) + { + await JS.InvokeVoidAsync("alert", "고객을 선택하세요."); + return; + } + var newId = await ContractClient.CreateAsync(contractForm.ClientId, contractForm.ContractNumber, contractForm.ServiceType, contractForm.StartDate ?? DateTime.Today, contractForm.MonthlyFee); if (newId > 0) { - Snackbar.Add("계약이 추가되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "계약이 추가되었습니다."); CloseDialog(); await LoadData(); } } catch (Exception ex) { - Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}"); } } private async Task DeleteContract(int id) { - var parameters = new DialogParameters - { - { "Title", "삭제 확인" }, - { "Message", "이 계약을 삭제하시겠습니까?" } - }; - - var dialog = await DialogService.ShowAsync("", parameters); - var result = await dialog.Result; - - if (result?.Canceled ?? true) - return; - + if (!await JS.InvokeAsync("confirm", "이 계약을 삭제하시겠습니까?")) return; try { await ContractClient.DeleteAsync(id); - Snackbar.Add("계약이 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "계약이 삭제되었습니다."); await LoadData(); } catch (Exception ex) { - Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}"); } } - private void CloseDialog() - { - isDialogOpen = false; - 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; } - public string ContractNumber { get; set; } = ""; - public string ServiceType { get; set; } = ""; - public DateTime? StartDate { get; set; } - public decimal? MonthlyFee { get; set; } - } + private void CloseDialog() { isDialogOpen = false; contractForm = new(); } + private static string GetClientDisplayName(Client client) => !string.IsNullOrWhiteSpace(client.CompanyName) ? client.CompanyName : !string.IsNullOrWhiteSpace(client.Name) ? client.Name : $"Client #{client.Id}"; + private sealed 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/Dashboard.razor b/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor index 993a391..0b55d62 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor @@ -8,207 +8,205 @@
- Overview - 대시보드 - 문의 흐름과 콘텐츠 상태를 한 화면에서 확인합니다. +
Overview
+

대시보드

+

문의 흐름과 콘텐츠 상태를 한 화면에서 확인합니다.

- - 새 포스트 작성 - +
- -
-
-
- 이번달 문의 -
- @summary.ThisMonthInquiries - 💬 -
- 월간 상담 유입 (클릭 시 이동) -
-
- -
-
- 신규 문의 -
- @summary.NewInquiries - ⚠️ -
- 처리 대기 (클릭 시 이동) -
-
- -
-
- 전체 포스트 -
- @summary.TotalPosts - 📄 -
- 콘텐츠 자산 (클릭 시 이동) -
-
- -
-
- 발행된 포스트 -
- @summary.PublishedPosts - 🌐 -
- 검색 노출 대상 (클릭 시 이동) -
-
-
- -@if (upcomingFilings.Count > 0) +@if (summary is null) { - -
-
- 이번 달 마감 임박 신고 - 30일 이내 신고 예정 건 (고객명 클릭 시 상세 카드로 연결) +
+ +
+
+ +
+
+ +
+} +else +{ +
+
+
+ 이번달 문의 +
+ @summary.ThisMonthInquiries + 💬 +
+ 월간 상담 유입
- 전체 일정 보기
- - - - 고객 - 신고 유형 - 기한 - D-day - - - - @foreach (var f in upcomingFilings) - { - var dday = (f.DueDate.Date - DateTime.Today).Days; - - - - @f.ClientName - - - @f.FilingType - @f.DueDate.ToString("yyyy-MM-dd") - - @if (dday < 0) - { - 기한 초과 (@(-dday)일) - } - else if (dday <= 7) - { - D-@dday - } - else - { - D-@dday - } - - - } - - - + +
+
+ 신규 문의 +
+ @summary.NewInquiries + ⚠️ +
+ 처리 대기 +
+
+ +
+
+ 전체 포스트 +
+ @summary.TotalPosts + 📄 +
+ 콘텐츠 자산 +
+
+ +
+
+ 발행된 포스트 +
+ @summary.PublishedPosts + 🌐 +
+ 검색 노출 대상 +
+
+
} - -
-
- 최근 문의 - 최근 유입된 상담 요청을 빠르게 확인합니다. (이름 클릭 시 상세 관리 화면으로 연계) +@if (upcomingFilings.Count == 0) +{ +
이번 달 마감 임박 신고가 없습니다.
+} +else +{ +
+
+
+

이번 달 마감 임박 신고

+

30일 이내 신고 예정 건

+
+ 전체 일정 보기 +
+
+ + + + + + + + + + + @foreach (var f in upcomingFilings) + { + var dday = (f.DueDate.Date - DateTime.Today).Days; + + + + + + + } + +
고객신고 유형기한D-day
@f.ClientName@f.FilingType@f.DueDate.ToString("yyyy-MM-dd") + @if (dday < 0) + { + 기한 초과 (@(-dday)일) + } + else if (dday <= 7) + { + D-@dday + } + else + { + D-@dday + } +
- 문의 전체 보기
- - - - 이름 - 전화 - 분야 - 상태 - 날짜 - - - - @foreach (var inquiry in summary.RecentInquiries) - { - - - - @inquiry.Name - - - @inquiry.Phone - @inquiry.ServiceType - - - @GetStatusLabel(inquiry.Status) - - - @inquiry.CreatedAt.ToString("yyyy-MM-dd") - - } - - - +} + +@if (summary is not null) +{ +
+
+
+

최근 문의

+

최근 유입된 상담 요청을 빠르게 확인합니다.

+
+ 문의 전체 보기 +
+
+ + + + + + + + + + + + @foreach (var inquiry in summary.RecentInquiries) + { + + + + + + + + } + +
이름전화분야상태날짜
@inquiry.Name@inquiry.Phone@inquiry.ServiceType@GetStatusLabel(inquiry.Status)@inquiry.CreatedAt.ToString("yyyy-MM-dd")
+
+
+} @code { [CascadingParameter] private Task? AuthStateTask { get; set; } - private AdminDashboardSummary summary = new(0, 0, 0, 0, []); + private AdminDashboardSummary? summary; private List upcomingFilings = []; - private string? errorMessage; - private bool isLoading = true; protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) + try { - try - { - // API 클라이언트 사용 (서비스 직접 호출 X) - var summaryTask = DashboardClient.GetSummaryAsync(); - var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30); - - await Task.WhenAll(summaryTask, filingsTask); - summary = await summaryTask; - upcomingFilings = (await filingsTask).ToList(); - } - catch (Exception ex) - { - errorMessage = "대시보드 데이터를 불러올 수 없습니다."; - Console.Error.WriteLine($"Dashboard error: {ex.Message}"); - } - finally - { - isLoading = false; - StateHasChanged(); - } + var summaryTask = DashboardClient.GetSummaryAsync(); + var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30); + await Task.WhenAll(summaryTask, filingsTask); + summary = await summaryTask; + upcomingFilings = (await filingsTask).ToList(); } + catch (Exception ex) + { + Console.Error.WriteLine($"Dashboard error: {ex.Message}"); + } + StateHasChanged(); } } } private static string GetStatusLabel(string status) => InquiryStatusMapper.Labels.GetValueOrDefault(status, status); - - private static Color StatusColor(string status) => status switch + private static string GetStatusClass(string status) => status switch { - "new" => Color.Warning, - "consulting" => Color.Info, - "contracted" => Color.Success, - "rejected" => Color.Error, - "closed" => Color.Dark, - _ => Color.Default + "new" => "warning", + "consulting" => "info", + "contracted" => "success", + "rejected" => "danger", + "closed" => "dark", + _ => "default" }; } diff --git a/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqEdit.razor index f19d520..5e4c6e3 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqEdit.razor @@ -5,85 +5,52 @@ @using TaxBaik.Domain.Entities @inject IFaqBrowserClient FaqClient @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS @(Id.HasValue ? "FAQ 수정" : "FAQ 등록") -
- 홈페이지 - @(Id.HasValue ? "FAQ 수정" : "FAQ 등록") +
홈페이지
+

@(Id.HasValue ? "FAQ 수정" : "FAQ 등록")

- 목록으로 +
- +
@if (isLoading) { - + } else { - - - - - - - - - - - @foreach (var cat in FaqService.Categories) - { - @cat - } - - - - - - - - - - - - @(isSaving ? "저장 중..." : "저장") - - - 취소 - - - - +
+ + + + + +
+ + +
+
} - +
@code { [Parameter] public int? Id { get; set; } - - private MudForm form = null!; private Faq faq = new() { SortOrder = 10, IsActive = true }; - private bool isValid; private bool isLoading = true; private bool isSaving; + private string SortOrderText { get => faq.SortOrder.ToString(); set => faq.SortOrder = int.TryParse(value, out var n) ? n : 0; } protected override async Task OnInitializedAsync() { @@ -94,7 +61,7 @@ var existing = await FaqClient.GetByIdAsync(Id.Value); if (existing is null) { - Snackbar.Add("FAQ를 찾을 수 없습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", "FAQ를 찾을 수 없습니다."); Navigation.NavigateTo("/taxbaik/admin/faqs"); return; } @@ -102,7 +69,7 @@ } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); Navigation.NavigateTo("/taxbaik/admin/faqs"); return; } @@ -112,33 +79,30 @@ private async Task SaveAsync() { - await form.Validate(); - if (!isValid) return; - isSaving = true; try { + if (string.IsNullOrWhiteSpace(faq.Question) || string.IsNullOrWhiteSpace(faq.Answer)) + { + await JS.InvokeVoidAsync("alert", "질문과 답변을 입력하세요."); + return; + } + if (Id.HasValue) { var result = await FaqClient.UpdateAsync(Id.Value, faq); - if (result != null) - Snackbar.Add("FAQ가 수정되었습니다.", Severity.Success); - else - Snackbar.Add("수정 실패", Severity.Error); + await JS.InvokeVoidAsync("alert", result != null ? "FAQ가 수정되었습니다." : "수정 실패"); } else { var result = await FaqClient.CreateAsync(faq); - if (result != null) - Snackbar.Add("FAQ가 등록되었습니다.", Severity.Success); - else - Snackbar.Add("등록 실패", Severity.Error); + await JS.InvokeVoidAsync("alert", result != null ? "FAQ가 등록되었습니다." : "등록 실패"); } Navigation.NavigateTo("/taxbaik/admin/faqs"); } catch (Exception ex) { - Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}"); } finally { diff --git a/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqList.razor b/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqList.razor index 678d87b..d207c1c 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Faqs/FaqList.razor @@ -4,95 +4,63 @@ @using TaxBaik.Domain.Entities @inject IFaqBrowserClient FaqClient @inject NavigationManager Navigation -@inject IDialogService DialogService -@inject ISnackbar Snackbar +@inject IJSRuntime JS FAQ 관리
- 홈페이지 - FAQ 관리 - 홈페이지 자주 묻는 질문을 등록하고 순서를 관리합니다. +
홈페이지
+

FAQ 관리

+

홈페이지 자주 묻는 질문을 등록하고 순서를 관리합니다.

- - FAQ 등록 - + FAQ 등록
- +
@if (faqs is null) { - + } else if (!faqs.Any()) { -
- - 등록된 FAQ가 없습니다. -
+
등록된 FAQ가 없습니다.
} else { - - - - 순서 - 질문 - 카테고리 - 상태 - - - - - @foreach (var item in faqs) - { +
+ + - - - - - + + + + + - } - - - - 총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개 - + + + @foreach (var item in faqs) + { + + + + + + + + } + +
- @item.SortOrder - - - @item.Question - - - @if (!string.IsNullOrEmpty(item.Category)) - { - @item.Category - } - - @if (item.IsActive) - { - 노출 중 - } - else - { - 비활성 - } - - - - 수정 - - - 삭제 - - - 순서질문카테고리상태
@item.SortOrder@item.Question@(string.IsNullOrEmpty(item.Category) ? "" : item.Category)@(item.IsActive ? "노출 중" : "비활성") +
+ + +
+
+
+
총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
} - +
@code { [CascadingParameter] @@ -102,16 +70,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (firstRender && AuthStateTask != null) { - if (AuthStateTask != null) + var authState = await AuthStateTask; + if (authState.User.Identity?.IsAuthenticated == true) { - var authState = await AuthStateTask; - if (authState.User.Identity?.IsAuthenticated == true) - { - await LoadAsync(); - StateHasChanged(); - } + await LoadAsync(); + StateHasChanged(); } } } @@ -124,36 +89,32 @@ } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); faqs = []; } } private async Task DeleteAsync(Faq item) { - var confirmed = await DialogService.ShowMessageBox( - "FAQ 삭제", - $"'{item.Question}' 항목을 삭제하시겠습니까?", - yesText: "삭제", cancelText: "취소"); - - if (confirmed != true) return; + var confirmed = await JS.InvokeAsync("confirm", $"'{item.Question}' 항목을 삭제하시겠습니까?"); + if (!confirmed) return; try { var success = await FaqClient.DeleteAsync(item.Id); if (success) { - Snackbar.Add("FAQ가 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "FAQ가 삭제되었습니다."); await LoadAsync(); } else { - Snackbar.Add("삭제 실패", Severity.Error); + await JS.InvokeVoidAsync("alert", "삭제 실패"); } } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); } } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor index dbe453e..cace22a 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor @@ -5,51 +5,41 @@ @using TaxBaik.Web.Components.Admin.Forms @inject InquiryService InquiryService @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS 문의 등록
- Customer Relations - 새 문의 등록 - 고객 문의를 등록합니다. (전화, 오프라인 등) +
Customer Relations
+

새 문의 등록

+

고객 문의를 등록합니다. (전화, 오프라인 등)

- 취소 +
- +
- +
@code { - private void GoBack() - { - Navigation.NavigateTo("/taxbaik/admin/inquiries"); - } + private void GoBack() => Navigation.NavigateTo("/taxbaik/admin/inquiries"); private async Task HandleCreate(InquiryForm.InquiryFormModel model) { try { - await InquiryService.SubmitAsync( - model.Name, - model.Phone, - model.ServiceType, - model.Message, - model.Email, - ipAddress: "admin-registered"); - - Snackbar.Add("문의가 등록되었습니다.", Severity.Success); + await InquiryService.SubmitAsync(model.Name, model.Phone, model.ServiceType, model.Message, model.Email, ipAddress: "admin-registered"); + await JS.InvokeVoidAsync("alert", "문의가 등록되었습니다."); Navigation.NavigateTo("/taxbaik/admin/inquiries"); } catch (ValidationException ex) { - Snackbar.Add(ex.Message, Severity.Error); + await JS.InvokeVoidAsync("alert", ex.Message); } catch (Exception ex) { - Snackbar.Add($"등록 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"등록 실패: {ex.Message}"); } } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryDetail.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryDetail.razor index f974cf1..5f036f3 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryDetail.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryDetail.razor @@ -3,113 +3,75 @@ @using TaxBaik.Web.Services @inject IInquiryBrowserClient InquiryClient @inject NavigationManager Navigation -@inject ISnackbar Snackbar +@inject IJSRuntime JS 문의 상세
- Inquiry Details - 문의 상세 - 문의 정보를 확인하고 처리 상태를 관리합니다. +
Inquiry Details
+

문의 상세

+

문의 정보를 확인하고 처리 상태를 관리합니다.

@if (inquiry != null) { - - 문의 목록으로 - +
+ +
- - - - 문의 정보 - - - 이름 - @inquiry.Name - - - 연락처 - @inquiry.Phone - - - 이메일 - @(inquiry.Email ?? "-") - - - 분야 - @inquiry.ServiceType - - - 문의 내용 - - @inquiry.Message - - - - 접수일시 - @inquiry.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm") - - - +
+
+

문의 정보

+
+
이름@inquiry.Name
+
연락처@inquiry.Phone
+
이메일@(inquiry.Email ?? "-")
+
분야@inquiry.ServiceType
+
문의 내용@inquiry.Message
+
접수일시@inquiry.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm")
+
+
- - 담당자 메모 - - 메모 저장 - - +
+

담당자 메모

+ +
+ +
+
- - - 처리 상태 - - @foreach (var (key, label) in InquiryStatusMapper.Labels) - { - - @label - - } - - +
+

처리 상태

+
+ @foreach (var (key, label) in InquiryStatusMapper.Labels) + { + + } +
+
- @if (inquiry.ClientId == null) - { - - 고객 카드 생성 - 이 문의를 고객 카드로 등록합니다. - - 고객으로 등록 - - - } - else - { - - 연결된 고객 - - 고객 카드 보기 - - - } -
- + @if (inquiry.ClientId == null) + { +
+

고객 카드 생성

+

이 문의를 고객 카드로 등록합니다.

+ +
+ } + else + { +
+

연결된 고객

+ 고객 카드 보기 +
+ } +
} else { - 문의를 찾을 수 없습니다. +
문의를 찾을 수 없습니다.
} @code { @@ -134,16 +96,16 @@ else if (success) { inquiry.Status = status; - Snackbar.Add("상태가 변경되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "상태가 변경되었습니다."); } else { - Snackbar.Add("상태 변경에 실패했습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", "상태 변경에 실패했습니다."); } } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); } } @@ -156,16 +118,16 @@ else if (success) { inquiry.AdminMemo = adminMemo; - Snackbar.Add("메모가 저장되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "메모가 저장되었습니다."); } else { - Snackbar.Add("메모 저장에 실패했습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", "메모 저장에 실패했습니다."); } } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); } } @@ -184,26 +146,19 @@ else { inquiry.ClientId = clientId; inquiry.Status = "consulting"; - Snackbar.Add("고객 카드가 생성되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "고객 카드가 생성되었습니다."); } else { - Snackbar.Add("고객 카드 생성에 실패했습니다.", Severity.Error); + await JS.InvokeVoidAsync("alert", "고객 카드 생성에 실패했습니다."); } } catch (Exception ex) { - Snackbar.Add($"오류: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}"); } } - private Color StatusColor(string status) => status switch - { - "new" => Color.Default, - "consulting" => Color.Info, - "contracted" => Color.Success, - "rejected" => Color.Error, - "closed" => Color.Dark, - _ => Color.Default - }; + private string GetStatusButtonClass(string status) + => inquiry?.Status == status ? "site-button primary" : "site-button secondary"; } diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor index 5eb11ea..7dd7075 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor @@ -5,45 +5,39 @@ @using TaxBaik.Web.Components.Admin.Forms @inject InquiryService InquiryService @inject NavigationManager Navigation -@inject ISnackbar Snackbar -@inject IDialogService DialogService +@inject IJSRuntime JS 문의 수정
- Customer Relations - 문의 수정 - 고객 문의 정보를 수정합니다. +
Customer Relations
+

문의 수정

+

고객 문의 정보를 수정합니다.

- 취소 +
@if (isLoading) { - +
} else if (inquiry == null) { - 문의를 찾을 수 없습니다. +
문의를 찾을 수 없습니다.
} else { - +
- - - - - 문의 삭제 - - +
+ +
+
} @code { - [Parameter] - public int Id { get; set; } - + [Parameter] public int Id { get; set; } private Domain.Entities.Inquiry? inquiry; private InquiryForm.InquiryFormModel? formModel; private bool isLoading = true; @@ -69,7 +63,7 @@ else } catch (Exception ex) { - Snackbar.Add($"문의 로드 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"문의 로드 실패: {ex.Message}"); } finally { @@ -77,16 +71,11 @@ else } } - private void GoBack() - { - Navigation.NavigateTo("/taxbaik/admin/inquiries"); - } + private void GoBack() => Navigation.NavigateTo("/taxbaik/admin/inquiries"); private async Task HandleUpdate(InquiryForm.InquiryFormModel model) { - if (inquiry == null) - return; - + if (inquiry == null) return; try { inquiry.Name = model.Name; @@ -97,47 +86,35 @@ else inquiry.AdminMemo = model.AdminMemo; if (inquiry.Status != model.Status) - { await InquiryService.UpdateStatusAsync(inquiry.Id, model.Status); - } await InquiryService.UpdateAdminMemoAsync(inquiry.Id, model.AdminMemo); - - Snackbar.Add("문의가 수정되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "문의가 수정되었습니다."); Navigation.NavigateTo("/taxbaik/admin/inquiries"); } catch (ValidationException ex) { - Snackbar.Add(ex.Message, Severity.Error); + await JS.InvokeVoidAsync("alert", ex.Message); } catch (Exception ex) { - Snackbar.Add($"수정 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"수정 실패: {ex.Message}"); } } private async Task DeleteInquiry() { - if (inquiry == null) - return; - - var result = await DialogService.ShowMessageBox( - "문의 삭제", - "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", - "삭제", "취소"); - - if (result != true) - return; - + if (inquiry == null) return; + if (!await JS.InvokeAsync("confirm", "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.")) return; try { await InquiryService.DeleteAsync(inquiry.Id); - Snackbar.Add("문의가 삭제되었습니다.", Severity.Success); + await JS.InvokeVoidAsync("alert", "문의가 삭제되었습니다."); Navigation.NavigateTo("/taxbaik/admin/inquiries"); } catch (Exception ex) { - Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); + await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}"); } } } diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor index 7a40ff8..af6c90a 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor @@ -7,47 +7,36 @@
- Customer Requests - 문의 관리 - 상담 요청을 상태별로 확인하고 후속 조치를 기록합니다. +
Customer Requests
+

문의 관리

+

상담 요청을 상태별로 확인하고 후속 조치를 기록합니다.

- 새 문의 등록 +
- -@if (isLoading) -{ - -} -else -{ - - - - - - - - - - - - - - - - - - - - -} - +
+ @if (isLoading) + { + + } + else + { +
+ + + + + + +
+ + } +
@code { [CascadingParameter] private Task? AuthStateTask { get; set; } + [Inject] private NavigationManager Navigation { get; set; } = default!; private bool isLoading = true; private IReadOnlyList allInquiries = []; diff --git a/TaxBaik.Web/Components/Admin/Pages/Login.razor b/TaxBaik.Web/Components/Admin/Pages/Login.razor index 5ea44a0..993b39c 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Login.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Login.razor @@ -1,4 +1,5 @@ @page "/admin/login" +@using Microsoft.FluentUI.AspNetCore.Components @using System.ComponentModel.DataAnnotations @layout TaxBaik.Web.Components.Admin.Layout.BlankLayout @attribute [AllowAnonymous] @@ -10,41 +11,39 @@ 로그인 -