Refactor admin dashboard and standardize UI shells
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m46s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m46s
This commit is contained in:
@@ -12,14 +12,9 @@
|
||||
|
||||
<PageTitle>@(Id.HasValue ? "공지 수정" : "공지 등록")</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Homepage</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "공지 수정" : "공지 등록")</MudText>
|
||||
</div>
|
||||
</section>
|
||||
<AdminPageHeader Title="@(Id.HasValue ? "공지 수정" : "공지 등록")" Eyebrow="Homepage" />
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminEditorPanel Loading="@isSaving" SkeletonContent="@null">
|
||||
<MudForm @ref="form">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
@@ -89,7 +84,7 @@
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
@@ -10,23 +10,20 @@
|
||||
|
||||
<PageTitle>공지사항 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Homepage</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">공지사항 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">홈페이지 상단에 노출되는 공지사항을 등록하고 관리합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="공지사항 관리" Eyebrow="Homepage" Subtitle="홈페이지 상단에 노출되는 공지사항을 등록하고 관리합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
Href="/taxbaik/admin/announcements/create">
|
||||
공지 등록
|
||||
</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="d-flex pa-4 gap-4 align-center">
|
||||
<AdminSearchBar>
|
||||
<SearchContent>
|
||||
<MudTextField @bind-Value="searchQuery" Placeholder="공지사항 제목 검색..." Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||
</div>
|
||||
</SearchContent>
|
||||
</AdminSearchBar>
|
||||
|
||||
<AdminDataPanel Loading="@(announcements is null)" SkeletonContent="@AnnouncementSkeleton">
|
||||
@if (announcements is null)
|
||||
|
||||
@@ -11,13 +11,7 @@
|
||||
|
||||
<PageTitle>고객 상세</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Client Details</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">고객 상세</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 정보와 상담 이력을 관리합니다.</MudText>
|
||||
</div>
|
||||
</section>
|
||||
<AdminPageHeader Title="고객 상세" Eyebrow="Client Details" Subtitle="고객 정보와 상담 이력을 관리합니다." />
|
||||
|
||||
@if (client == null)
|
||||
{
|
||||
|
||||
@@ -12,14 +12,10 @@
|
||||
|
||||
<PageTitle>@(Id.HasValue ? "고객 수정" : "고객 등록")</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "고객 수정" : "고객 등록")</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="@(Id.HasValue ? "고객 수정" : "고객 등록")" Eyebrow="CRM">
|
||||
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/clients"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@ClientEditSkeleton">
|
||||
@if (isLoading)
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
</AdminPageHeader>
|
||||
|
||||
@* 검색/필터 바 *@
|
||||
<MudPaper Class="admin-surface mb-3 pa-3" Elevation="0">
|
||||
<AdminSearchBar>
|
||||
<SearchContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="5">
|
||||
<MudTextField @bind-Value="searchText" Label="검색 (이름·연락처·회사명)"
|
||||
@@ -31,14 +32,13 @@
|
||||
<MudItem xs="12" md="3">
|
||||
<CommonCodeSelect @bind-Value="statusFilter" Group="CLIENT_STATUS" Label="상태" Placeholder="전체" Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2" Class="d-flex align-center">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@SearchAsync" FullWidth="true">검색</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2" Class="d-flex align-center">
|
||||
<MudButton Variant="Variant.Text" OnClick="@ResetAsync" FullWidth="true">초기화</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</SearchContent>
|
||||
<Actions>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@SearchAsync">검색</MudButton>
|
||||
<MudButton Variant="Variant.Text" OnClick="@ResetAsync">초기화</MudButton>
|
||||
</Actions>
|
||||
</AdminSearchBar>
|
||||
|
||||
<AdminDataPanel Loading="@(clients is null)" SkeletonContent="@ClientListSkeleton">
|
||||
@if (clients is null)
|
||||
|
||||
@@ -8,14 +8,9 @@
|
||||
|
||||
<PageTitle>고객사 등록</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Settings</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">새 고객사 등록</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">새로운 고객사를 추가합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="새 고객사 등록" Eyebrow="Settings" Subtitle="새로운 고객사를 추가합니다.">
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<CompanyForm ButtonText="등록" OnSubmit="HandleCreate" OnCancel="GoBack" />
|
||||
|
||||
@@ -9,14 +9,9 @@
|
||||
|
||||
<PageTitle>고객사 수정</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Settings</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">고객사 수정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객사 정보를 수정합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="고객사 수정" Eyebrow="Settings" Subtitle="고객사 정보를 수정합니다.">
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@CompanySkeleton">
|
||||
@if (formModel == null)
|
||||
|
||||
@@ -6,15 +6,10 @@
|
||||
|
||||
<PageTitle>고객사 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Settings</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">고객사 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">등록된 고객사를 관리하고 새로운 고객사를 추가합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="고객사 관리" Eyebrow="Settings" Subtitle="등록된 고객사를 관리하고 새로운 고객사를 추가합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||
Href="/taxbaik/admin/companies/create">새 고객사 등록</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<MudPaper Class="admin-surface mb-4 mt-4" Elevation="0">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
|
||||
<PageTitle>상담 활동 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">상담 활동 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">초기 렌더 진단용 최소 화면입니다.</MudText>
|
||||
</div>
|
||||
</section>
|
||||
<AdminPageHeader Title="상담 활동 관리" Eyebrow="CRM & 세무관리" Subtitle="초기 렌더 진단용 최소 화면입니다." />
|
||||
|
||||
<AdminDataPanel Loading="@(activities is null)" SkeletonContent="@ActivitySkeleton">
|
||||
@if (activities is null)
|
||||
|
||||
@@ -10,23 +10,17 @@
|
||||
|
||||
<PageTitle>계약 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">계약 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 계약과 월 정기수익을 함께 관리합니다.</MudText>
|
||||
<AdminPageHeader Title="계약 관리" Eyebrow="CRM & 세무관리" Subtitle="고객 계약과 월 정기수익을 함께 관리합니다.">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@if (mrr > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
월 정기수익:
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">₩@mrr.ToString("N0")</MudChip>
|
||||
</MudText>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">월 정기수익 ₩@mrr.ToString("N0")</MudChip>
|
||||
}
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-contract">
|
||||
새 계약 추가
|
||||
</MudButton>
|
||||
</section>
|
||||
</MudStack>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminEditorPanel Loading="@(contracts is null)" SkeletonContent="@ContractSkeleton">
|
||||
@if (contracts is null)
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
@page "/admin/dashboard"
|
||||
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>대시보드</PageTitle>
|
||||
|
||||
<section style="padding: 24px;">
|
||||
<h1 style="margin: 0 0 12px 0; font-size: 1.8rem;">대시보드</h1>
|
||||
<p style="margin: 0 0 12px 0;">관리자 SPA 부팅 검증용 최소 화면입니다.</p>
|
||||
<ul>
|
||||
<li>WASM 부팅 성공 여부</li>
|
||||
<li>라우팅 성공 여부</li>
|
||||
<li>JS interop 예외 여부</li>
|
||||
</ul>
|
||||
</section>
|
||||
<DashboardOverview />
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
@inject TaxBaik.Web.Client.Components.Admin.Services.IAdminDashboardClient DashboardClient
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>대시보드</PageTitle>
|
||||
|
||||
<AdminPageHeader Title="대시보드" Eyebrow="Home" Subtitle="운영 현황과 바로가기 작업을 한 화면에서 확인합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh" Disabled="@loading" @onclick="ReloadAsync">
|
||||
새로고침
|
||||
</MudButton>
|
||||
</AdminPageHeader>
|
||||
|
||||
<MudGrid Class="admin-dashboard-grid">
|
||||
@if (loading)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<AdminDataPanel Loading="true" />
|
||||
</MudItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<AdminMetricCard Label="이번 달 문의" Value="@summary?.ThisMonthInquiries" Caption="이번 달 접수된 문의" Accent="accent-blue" Icon="@Icons.Material.Filled.MarkUnreadChatAlt" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<AdminMetricCard Label="신규 문의" Value="@summary?.NewInquiries" Caption="처리 대기 중인 문의" Accent="accent-amber" Icon="@Icons.Material.Filled.PriorityHigh" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<AdminMetricCard Label="총 게시글" Value="@summary?.TotalPosts" Caption="홈페이지 블로그 누적" Accent="accent-slate" Icon="@Icons.Material.Filled.Article" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<AdminMetricCard Label="게시중 글" Value="@summary?.PublishedPosts" Caption="현재 공개 중인 글" Accent="accent-green" Icon="@Icons.Material.Filled.Public" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="5">
|
||||
<AdminDataPanel>
|
||||
<AdminPageHeader Title="빠른 작업" Subtitle="자주 쓰는 관리 화면으로 바로 이동합니다." />
|
||||
<MudStack Spacing="1">
|
||||
@foreach (var action in QuickActions)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true" StartIcon="@action.Icon" OnClick="@(() => Navigation.NavigateTo(action.Href))">
|
||||
@action.Label
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</AdminDataPanel>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="7">
|
||||
<AdminDataPanel>
|
||||
<AdminPageHeader Title="최근 문의" Subtitle="최신 처리 흐름을 빠르게 확인합니다." />
|
||||
@if (summary?.RecentInquiries is null || !summary.RecentInquiries.Any())
|
||||
{
|
||||
<AdminEmptyState Icon="@Icons.Material.Filled.Inbox" Message="새로운 문의가 들어오면 이 영역에 표시됩니다." />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSimpleTable Dense="true" Class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>제목</th>
|
||||
<th>상태</th>
|
||||
<th>접수일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var inquiry in summary.RecentInquiries)
|
||||
{
|
||||
<tr>
|
||||
<td>@inquiry.Name</td>
|
||||
<td>@inquiry.Status</td>
|
||||
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
}
|
||||
</AdminDataPanel>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<AdminDataPanel Loading="@statsLoading">
|
||||
<AdminPageHeader Title="월간 통계" Subtitle="문의 처리 전환과 상태 분포를 확인합니다." />
|
||||
@if (monthlyStats is not null)
|
||||
{
|
||||
<div class="admin-stats-grid">
|
||||
<div class="admin-stat">
|
||||
<span class="admin-stat-label">총 문의</span>
|
||||
<strong class="admin-stat-value">@monthlyStats.TotalInquiries</strong>
|
||||
</div>
|
||||
<div class="admin-stat">
|
||||
<span class="admin-stat-label">상담 중</span>
|
||||
<strong class="admin-stat-value">@monthlyStats.ConsultingCount</strong>
|
||||
</div>
|
||||
<div class="admin-stat">
|
||||
<span class="admin-stat-label">완료</span>
|
||||
<strong class="admin-stat-value">@monthlyStats.CompletedCount</strong>
|
||||
</div>
|
||||
<div class="admin-stat">
|
||||
<span class="admin-stat-label">완료율</span>
|
||||
<strong class="admin-stat-value">@monthlyStats.CompletionRate.ToString("0.#")%</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-chart">
|
||||
<div class="admin-chart-header">
|
||||
<span>최근 6개월 문의 추세</span>
|
||||
<span>@monthlyStats.Month</span>
|
||||
</div>
|
||||
<div class="admin-chart-bars">
|
||||
@foreach (var point in trendPoints)
|
||||
{
|
||||
<div class="admin-chart-row">
|
||||
<span>@point.Label</span>
|
||||
<div class="admin-chart-track">
|
||||
<div class="admin-chart-fill accent-blue" style="width:@BarWidth(point.Total, trendMax)"></div>
|
||||
</div>
|
||||
<span>@point.Total</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</AdminDataPanel>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<AdminDataPanel>
|
||||
<AdminPageHeader Title="바로가기" Subtitle="운영 중 자주 사용하는 화면으로 이동합니다." />
|
||||
<div class="admin-quick-grid">
|
||||
@foreach (var action in QuickActions)
|
||||
{
|
||||
<MudPaper Class="admin-quick-card" Elevation="0" @onclick="@(() => Navigation.NavigateTo(action.Href))">
|
||||
<MudIcon Icon="@action.Icon" />
|
||||
<span>@action.Label</span>
|
||||
</MudPaper>
|
||||
}
|
||||
</div>
|
||||
</AdminDataPanel>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<AdminDataPanel>
|
||||
<AdminPageHeader Title="운영 메모" Subtitle="이번 배포에서 확인해야 할 핵심 포인트입니다." />
|
||||
<div class="admin-note-list">
|
||||
<div>대시보드와 메뉴가 역할 기반으로 필터링됩니다.</div>
|
||||
<div>공통 CRUD 화면은 `AdminPageHeader`/`AdminCrudPageShell` 슬롯 패턴을 따릅니다.</div>
|
||||
<div>월간 통계는 문의 전환 흐름을 빠르게 보는 용도입니다.</div>
|
||||
</div>
|
||||
</AdminDataPanel>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<AdminDataPanel>
|
||||
<AdminPageHeader Title="알림" Subtitle="운영 상태를 빠르게 점검합니다." />
|
||||
<MudStack Spacing="1">
|
||||
<MudAlert Severity="@(summary is not null && summary.NewInquiries > 0 ? Severity.Warning : Severity.Info)">
|
||||
@(summary is not null && summary.NewInquiries > 0
|
||||
? $"처리 대기 중인 신규 문의가 {summary.NewInquiries}건 있습니다."
|
||||
: "현재 신규 문의가 없습니다.")
|
||||
</MudAlert>
|
||||
<MudAlert Severity="@(summary is not null && summary.PublishedPosts < summary.TotalPosts ? Severity.Info : Severity.Success)">
|
||||
@(summary is not null && summary.PublishedPosts < summary.TotalPosts
|
||||
? "비공개 중인 게시글이 있어 발행 상태 확인이 필요합니다."
|
||||
: "게시글 발행 상태가 안정적입니다.")
|
||||
</MudAlert>
|
||||
</MudStack>
|
||||
</AdminDataPanel>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@code {
|
||||
private bool loading = true;
|
||||
private bool statsLoading = true;
|
||||
private AdminDashboardSummary? summary;
|
||||
private TaxBaik.Web.Client.Components.Admin.Services.MonthlyStatsDto? monthlyStats;
|
||||
private readonly List<(string Label, int Total)> trendPoints = [];
|
||||
|
||||
private static readonly (string Label, string Href, string Icon)[] QuickActions =
|
||||
[
|
||||
("공지 등록", "/taxbaik/admin/announcements/create", Icons.Material.Filled.Campaign),
|
||||
("고객사 등록", "/taxbaik/admin/companies/create", Icons.Material.Filled.Business),
|
||||
("문의 관리", "/taxbaik/admin/inquiries", Icons.Material.Filled.Inbox),
|
||||
("설정 열기", "/taxbaik/admin/settings", Icons.Material.Filled.Settings),
|
||||
];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAsync();
|
||||
await LoadStatsAsync();
|
||||
await LoadTrendAsync();
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
summary = await DashboardClient.GetSummaryAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadStatsAsync()
|
||||
{
|
||||
statsLoading = true;
|
||||
try
|
||||
{
|
||||
monthlyStats = await DashboardClient.GetMonthlyStatsAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
statsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadTrendAsync()
|
||||
{
|
||||
trendPoints.Clear();
|
||||
var today = DateTime.Today;
|
||||
for (var i = 5; i >= 0; i--)
|
||||
{
|
||||
var month = today.AddMonths(-i);
|
||||
var monthly = await DashboardClient.GetMonthlyStatsAsync(month.ToString("yyyy-MM"));
|
||||
trendPoints.Add(($"{month:MM}", monthly.TotalInquiries));
|
||||
}
|
||||
}
|
||||
|
||||
private static string BarWidth(int value, int total)
|
||||
=> total <= 0 ? "0%" : $"{Math.Max(6, (int)Math.Round(value * 100.0 / total))}%";
|
||||
|
||||
private int trendMax => Math.Max(1, trendPoints.Count == 0 ? 1 : trendPoints.Max(x => x.Total));
|
||||
}
|
||||
@@ -9,14 +9,10 @@
|
||||
|
||||
<PageTitle>@(Id.HasValue ? "FAQ 수정" : "FAQ 등록")</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">홈페이지</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "FAQ 수정" : "FAQ 등록")</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="@(Id.HasValue ? "FAQ 수정" : "FAQ 등록")" Eyebrow="홈페이지">
|
||||
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/faqs"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@FaqSkeleton">
|
||||
<MudPaper Class="admin-surface" Elevation="0" Style="max-width:720px;">
|
||||
|
||||
@@ -10,23 +10,20 @@
|
||||
|
||||
<PageTitle>FAQ 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">홈페이지</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">FAQ 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">홈페이지 자주 묻는 질문을 등록하고 순서를 관리합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="FAQ 관리" Eyebrow="홈페이지" Subtitle="홈페이지 자주 묻는 질문을 등록하고 순서를 관리합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
Href="/taxbaik/admin/faqs/create">
|
||||
FAQ 등록
|
||||
</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="d-flex pa-4 gap-4 align-center">
|
||||
<AdminSearchBar>
|
||||
<SearchContent>
|
||||
<MudTextField @bind-Value="searchQuery" Placeholder="질문 또는 답변 검색..." Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||
</div>
|
||||
</SearchContent>
|
||||
</AdminSearchBar>
|
||||
|
||||
<AdminDataPanel Loading="@(faqs is null)" SkeletonContent="@FaqListSkeleton">
|
||||
@if (faqs is null)
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
|
||||
<PageTitle>문의 상세</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Inquiry Details</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">문의 상세</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">문의 정보를 확인하고 처리 상태를 관리합니다.</MudText>
|
||||
</div>
|
||||
</section>
|
||||
<AdminPageHeader Title="문의 상세" Eyebrow="Inquiry Details" Subtitle="문의 정보를 확인하고 처리 상태를 관리합니다." />
|
||||
|
||||
@if (inquiry != null)
|
||||
{
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
|
||||
<PageTitle>수익 추적 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">수익 추적 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">초기 렌더 진단용 최소 화면입니다.</MudText>
|
||||
</div>
|
||||
</section>
|
||||
<AdminPageHeader Title="수익 추적 관리" Eyebrow="CRM & 세무관리" Subtitle="청구 기록과 수익 흐름을 확인합니다." />
|
||||
|
||||
<AdminDataPanel Loading="@(revenues is null)" SkeletonContent="@RevenueSkeleton">
|
||||
@if (revenues is null)
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
|
||||
<PageTitle>시즌 시뮬레이터</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Season Preview</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">시즌 시뮬레이터</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">날짜를 선택해 홈페이지 시즌 화면이 어떻게 보이는지 미리 확인합니다.</MudText>
|
||||
</div>
|
||||
</section>
|
||||
<AdminPageHeader Title="시즌 시뮬레이터" Eyebrow="Season Preview" Subtitle="날짜를 선택해 홈페이지 시즌 화면이 어떻게 보이는지 미리 확인합니다." />
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
|
||||
@@ -9,19 +9,11 @@
|
||||
|
||||
<PageTitle>설정</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="pa-6">
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">System</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">설정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">공개 사이트 연락처와 관리자 계정 보안을 관리합니다.</MudText>
|
||||
</div>
|
||||
</section>
|
||||
</MudContainer>
|
||||
<AdminPageHeader Title="설정" Eyebrow="System" Subtitle="공개 사이트 연락처와 관리자 계정 보안을 관리합니다." />
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="7">
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminEditorPanel Loading="@isLoadingSettings">
|
||||
<div class="admin-section-header compact">
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">사이트 정보</MudText>
|
||||
@@ -45,11 +37,11 @@
|
||||
StartIcon="@Icons.Material.Filled.Save"
|
||||
@onclick="SaveSettings">사이트 정보 저장</MudButton>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</AdminEditorPanel>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="5">
|
||||
<MudPaper Class="admin-surface admin-account-card" Elevation="0">
|
||||
<AdminEditorPanel Loading="@isLoadingSettings">
|
||||
<div class="admin-section-header compact">
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">계정 관리</MudText>
|
||||
@@ -74,7 +66,7 @@
|
||||
@(isChangingPassword ? "변경 중..." : "비밀번호 변경")
|
||||
</MudButton>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</AdminEditorPanel>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
|
||||
@@ -11,16 +11,11 @@
|
||||
|
||||
<PageTitle>신고 일정</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">신고 일정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 마감일과 처리 상태를 한 화면에서 관리합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="신고 일정" Eyebrow="CRM & 세무관리" Subtitle="고객별 마감일과 처리 상태를 한 화면에서 관리합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-schedule">
|
||||
새 일정 추가
|
||||
</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminDataPanel Loading="@(schedules is null)" SkeletonContent="@ScheduleSkeleton">
|
||||
@if (schedules is null)
|
||||
|
||||
@@ -10,18 +10,13 @@
|
||||
|
||||
<PageTitle>신고 일정 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Tax Schedule</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">신고 일정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 세금 신고 마감일을 관리하고 완료 처리합니다.</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="신고 일정" Eyebrow="Tax Schedule" Subtitle="고객별 세금 신고 마감일을 관리하고 완료 처리합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
OnClick="@(() => showAddForm = !showAddForm)"
|
||||
StartIcon="@Icons.Material.Filled.Add">
|
||||
일정 추가
|
||||
</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
@if (showAddForm)
|
||||
{
|
||||
|
||||
@@ -11,16 +11,11 @@
|
||||
|
||||
<PageTitle>세무 프로필</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">세무 프로필</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 세무 프로필, 신고 일정, 위험도 추적</MudText>
|
||||
</div>
|
||||
<AdminPageHeader Title="세무 프로필" Eyebrow="CRM & 세무관리" Subtitle="고객별 세무 프로필, 신고 일정, 위험도 추적">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-profile">
|
||||
새 프로필 추가
|
||||
</MudButton>
|
||||
</section>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminDataPanel Loading="@(profiles == null)" SkeletonContent="@ProfileSkeleton">
|
||||
@if (profiles == null)
|
||||
|
||||
@@ -15,7 +15,7 @@ public interface IAdminDashboardClient
|
||||
Task<AdminDashboardSummary> GetSummaryAsync(CancellationToken ct = default);
|
||||
Task<IEnumerable<TaxFiling>> GetUpcomingFilingsAsync(int days = 30, CancellationToken ct = default);
|
||||
Task<IEnumerable<Inquiry>> GetRecentInquiriesAsync(int limit = 10, CancellationToken ct = default);
|
||||
Task<object> GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default);
|
||||
Task<MonthlyStatsDto> GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class AdminDashboardClient : IAdminDashboardClient
|
||||
@@ -87,7 +87,7 @@ public class AdminDashboardClient : IAdminDashboardClient
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default)
|
||||
public async Task<MonthlyStatsDto> GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -96,8 +96,8 @@ public class AdminDashboardClient : IAdminDashboardClient
|
||||
if (!string.IsNullOrEmpty(month))
|
||||
url += $"?month={month}";
|
||||
|
||||
var result = await _http.GetFromJsonAsync<object>(url, cancellationToken: ct);
|
||||
return result ?? new();
|
||||
var result = await _http.GetFromJsonAsync<MonthlyStatsDto>(url, cancellationToken: ct);
|
||||
return result ?? new MonthlyStatsDto();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
@@ -107,6 +107,16 @@ public class AdminDashboardClient : IAdminDashboardClient
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MonthlyStatsDto
|
||||
{
|
||||
public string Month { get; set; } = string.Empty;
|
||||
public int TotalInquiries { get; set; }
|
||||
public int ConsultingCount { get; set; }
|
||||
public int CompletedCount { get; set; }
|
||||
public int NewCount { get; set; }
|
||||
public double CompletionRate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API Response wrapper
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<AdminPageHeader Title="@Title" Eyebrow="@Eyebrow" Subtitle="@Subtitle">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexEnd" Spacing="1">
|
||||
@if (Actions is not null)
|
||||
{
|
||||
@Actions
|
||||
}
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="OnCancel">
|
||||
@CancelText
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</AdminPageHeader>
|
||||
|
||||
<AdminEditorPanel Loading="@Loading" SkeletonContent="@SkeletonContent">
|
||||
@@ -24,6 +30,9 @@
|
||||
[Parameter]
|
||||
public string CancelText { get; set; } = "취소";
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Actions { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using MudBlazor;
|
||||
|
||||
namespace TaxBaik.Web.Client.Components.Admin.Shared;
|
||||
|
||||
public sealed record AdminNavigationItem(
|
||||
string Label,
|
||||
string Href,
|
||||
string Icon,
|
||||
string? Badge = null,
|
||||
bool IsExact = false,
|
||||
IReadOnlyList<string>? AllowedRoles = null);
|
||||
|
||||
public sealed record AdminNavigationSection(
|
||||
string Label,
|
||||
IReadOnlyList<AdminNavigationItem> Items);
|
||||
|
||||
public static class AdminNavigationCatalog
|
||||
{
|
||||
public static IReadOnlyList<AdminNavigationSection> Sections { get; } =
|
||||
[
|
||||
new("개요", [
|
||||
new("대시보드", "/taxbaik/admin/dashboard", Icons.Material.Filled.Dashboard, IsExact: true),
|
||||
]),
|
||||
new("CRM & 세무관리", [
|
||||
new("세무 프로필", "/taxbaik/admin/tax-profiles", Icons.Material.Filled.Badge, AllowedRoles: ["Admin", "Manager"]),
|
||||
new("신고 일정", "/taxbaik/admin/tax-filing-schedules", Icons.Material.Filled.EventNote, AllowedRoles: ["Admin", "Manager"]),
|
||||
new("계약 관리", "/taxbaik/admin/contracts", Icons.Material.Filled.Description, AllowedRoles: ["Admin", "Manager"]),
|
||||
new("상담 활동", "/taxbaik/admin/consulting-activities", Icons.Material.Filled.SupportAgent, AllowedRoles: ["Admin", "Manager"]),
|
||||
new("수익 추적", "/taxbaik/admin/revenue-trackings", Icons.Material.Filled.Payments, AllowedRoles: ["Admin"]),
|
||||
]),
|
||||
new("고객 관리", [
|
||||
new("고객 카드", "/taxbaik/admin/clients", Icons.Material.Filled.Groups, AllowedRoles: ["Admin", "Manager", "Staff"]),
|
||||
new("세무신고", "/taxbaik/admin/tax-filings", Icons.Material.Filled.Assignment, AllowedRoles: ["Admin", "Manager", "Staff"]),
|
||||
]),
|
||||
new("홈페이지", [
|
||||
new("공지사항", "/taxbaik/admin/announcements", Icons.Material.Filled.Campaign, AllowedRoles: ["Admin", "ContentManager"]),
|
||||
new("FAQ 관리", "/taxbaik/admin/faqs", Icons.Material.Filled.HelpOutline, AllowedRoles: ["Admin", "ContentManager"]),
|
||||
new("블로그 관리", "/taxbaik/admin/blog", Icons.Material.Filled.Article, AllowedRoles: ["Admin", "ContentManager"]),
|
||||
new("시즌 시뮬레이터", "/taxbaik/admin/season-simulator", Icons.Material.Filled.CalendarMonth, AllowedRoles: ["Admin", "ContentManager"]),
|
||||
new("문의 관리", "/taxbaik/admin/inquiries", Icons.Material.Filled.Inbox, AllowedRoles: ["Admin", "Manager", "Staff"]),
|
||||
new("설정", "/taxbaik/admin/settings", Icons.Material.Filled.Settings, AllowedRoles: ["Admin"]),
|
||||
new("공통관리", "/taxbaik/admin/common-codes", Icons.Material.Filled.Apps, AllowedRoles: ["Admin"]),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<MudPaper Class="admin-surface admin-search-bar" Elevation="0">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="admin-search-bar-inner">
|
||||
<div class="admin-search-field">
|
||||
@SearchContent
|
||||
</div>
|
||||
@if (Actions is not null)
|
||||
{
|
||||
<div class="admin-search-actions">
|
||||
@Actions
|
||||
</div>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public RenderFragment? SearchContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Actions { get; set; }
|
||||
}
|
||||
@@ -21,37 +21,7 @@
|
||||
<div class="admin-body">
|
||||
@if (drawerOpen)
|
||||
{
|
||||
<aside class="admin-drawer">
|
||||
<div class="admin-drawer-brand">
|
||||
<div class="admin-brand-mark">T</div>
|
||||
<div>
|
||||
<div class="admin-drawer-title">TaxBaik</div>
|
||||
<div class="admin-drawer-subtitle">세무 운영 콘솔</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="admin-nav">
|
||||
<MudNavMenu Dense="true">
|
||||
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All">대시보드</MudNavLink>
|
||||
<MudText Typo="Typo.caption" Class="admin-nav-group">CRM & 세무관리</MudText>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-profiles">세무 프로필</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-filing-schedules">신고 일정</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/contracts">계약 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/consulting-activities">상담 활동</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/revenue-trackings">수익 추적</MudNavLink>
|
||||
<MudText Typo="Typo.caption" Class="admin-nav-group">고객 관리</MudText>
|
||||
<MudNavLink Href="/taxbaik/admin/clients">고객 카드</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-filings">세무신고</MudNavLink>
|
||||
<MudText Typo="Typo.caption" Class="admin-nav-group">홈페이지</MudText>
|
||||
<MudNavLink Href="/taxbaik/admin/announcements">공지사항</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/faqs">FAQ 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/blog">블로그 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/season-simulator">시즌 시뮬레이터</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/inquiries">문의 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/settings">설정</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/common-codes">공통관리</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</nav>
|
||||
</aside>
|
||||
<AdminSidebar />
|
||||
}
|
||||
|
||||
<main class="admin-main">
|
||||
@@ -67,7 +37,6 @@
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private bool drawerOpen = true;
|
||||
private bool firstRenderCompleted;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -83,14 +52,6 @@
|
||||
drawerOpen = !drawerOpen;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && !firstRenderCompleted)
|
||||
{
|
||||
firstRenderCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Navigation.LocationChanged -= OnLocationChanged;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using TaxBaik.Web.Client.Components.Admin.Shared
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
<aside class="admin-drawer">
|
||||
<div class="admin-drawer-brand">
|
||||
<div class="admin-brand-mark">T</div>
|
||||
<div>
|
||||
<div class="admin-drawer-title">TaxBaik</div>
|
||||
<div class="admin-drawer-subtitle">세무 운영 콘솔</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="admin-nav" aria-label="관리자 메뉴">
|
||||
<MudNavMenu Dense="true">
|
||||
@foreach (var section in visibleSections)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="admin-nav-group">@section.Label</MudText>
|
||||
@foreach (var item in section.Items)
|
||||
{
|
||||
<MudNavLink Href="@item.Href" Match="@(item.IsExact ? NavLinkMatch.All : NavLinkMatch.Prefix)">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@item.Icon" Size="Size.Small" />
|
||||
<span>@item.Label</span>
|
||||
@if (!string.IsNullOrWhiteSpace(item.Badge))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Class="admin-nav-badge">@item.Badge</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudNavLink>
|
||||
}
|
||||
}
|
||||
</MudNavMenu>
|
||||
</nav>
|
||||
|
||||
<div class="admin-drawer-footer">
|
||||
<div class="admin-footer-item">
|
||||
<MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" />
|
||||
<MudLink Href="/taxbaik" Target="_blank" Underline="Underline.None">공개 사이트</MudLink>
|
||||
</div>
|
||||
<div class="admin-footer-item">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Logout" Size="Size.Small" />
|
||||
<MudLink Href="/taxbaik/admin/logout" Underline="Underline.None">로그아웃</MudLink>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@code {
|
||||
private IReadOnlyList<AdminNavigationSection> visibleSections = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
visibleSections = AdminNavigationCatalog.Sections
|
||||
.Select(section => section with
|
||||
{
|
||||
Items = section.Items.Where(item => item.AllowedRoles is null || item.AllowedRoles.Count == 0 || item.AllowedRoles.Any(user.IsInRole)).ToList()
|
||||
})
|
||||
.Where(section => section.Items.Count > 0)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class ValidateSitemapEndpoint : EndpointWithoutRequest
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin/validate/sitemap");
|
||||
Roles("admin");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
@@ -40,7 +40,7 @@ public class ValidateRssEndpoint : EndpointWithoutRequest
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin/validate/rss");
|
||||
Roles("admin");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
@@ -62,7 +62,7 @@ public class ValidateConsistencyEndpoint : EndpointWithoutRequest
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin/validate/consistency");
|
||||
Roles("admin");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class GetMonthlyStatsEndpoint : Endpoint<MonthlyStatsQuery, MonthlyStatsR
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin-dashboard/monthly-stats");
|
||||
Policies("Bearer");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(MonthlyStatsQuery request, CancellationToken ct)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class GetRecentInquiriesEndpoint : Endpoint<RecentInquiriesQuery, RecentI
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin-dashboard/recent-inquiries");
|
||||
Policies("Bearer");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RecentInquiriesQuery request, CancellationToken ct)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class GetSummaryEndpoint : EndpointWithoutRequest<AdminDashboardSummaryRe
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin-dashboard/summary");
|
||||
Policies("Bearer");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class GetUpcomingFilingsEndpoint : Endpoint<UpcomingFilingsQuery, Upcomin
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin-dashboard/upcoming-filings");
|
||||
Policies("Bearer");
|
||||
Policies("AdminOnly");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpcomingFilingsQuery request, CancellationToken ct)
|
||||
|
||||
@@ -69,6 +69,10 @@ builder.Services.AddAuthorization(opts =>
|
||||
opts.AddPolicy("Bearer", new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser()
|
||||
.Build());
|
||||
opts.AddPolicy("AdminOnly", new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireRole("Admin")
|
||||
.Build());
|
||||
});
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
@@ -173,7 +173,8 @@ public class AuthService
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, user.Username),
|
||||
new Claim("username", user.Username)
|
||||
new Claim("username", user.Username),
|
||||
new Claim(ClaimTypes.Role, "Admin")
|
||||
};
|
||||
|
||||
var accessToken = new JwtSecurityToken(
|
||||
|
||||
@@ -497,6 +497,9 @@ textarea:focus-visible {
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.admin-drawer-brand {
|
||||
@@ -539,6 +542,23 @@ textarea:focus-visible {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.admin-nav .mud-nav-link .mud-stack {
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-nav-badge {
|
||||
margin-left: auto;
|
||||
height: 18px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.admin-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-nav .mud-nav-group-header {
|
||||
min-height: 32px;
|
||||
font-size: 0.78rem;
|
||||
@@ -618,6 +638,130 @@ textarea:focus-visible {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-dashboard-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.admin-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-stat {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border-color-light);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.admin-stat-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.admin-chart {
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border-color-light);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.admin-chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.admin-chart-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-chart-row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr 28px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-chart-track {
|
||||
height: 10px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-chart-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.admin-quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-quick-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border-color-light);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
|
||||
background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.admin-quick-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--border-color-strong);
|
||||
}
|
||||
|
||||
.admin-quick-card .mud-icon-root {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.admin-note-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-note-list div {
|
||||
padding-left: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.admin-note-list div::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.55em;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 9999px;
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Dashboard Page Styles
|
||||
============================================================================ */
|
||||
|
||||
Reference in New Issue
Block a user