Harden admin telemetry and deployment safeguards
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m30s
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m30s
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@inject VersionInfo VersionInfo
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
@@ -12,6 +13,8 @@
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<script>
|
||||
window.taxbaikAdminBuildVersion = '@VersionInfo.Version';
|
||||
window.taxbaikAdminComponent = 'AdminApp';
|
||||
document.documentElement.classList.toggle(
|
||||
'admin-login-route',
|
||||
window.location.pathname.toLowerCase().endsWith('/admin/login'));
|
||||
@@ -38,6 +41,7 @@
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="js/admin-session.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script>window.taxbaikAdminSession?.initErrorLogging();</script>
|
||||
<script>window.taxbaikAdminSession?.bindLoginForm();</script>
|
||||
<script>window.taxbaikAdminSession?.watchReconnect();</script>
|
||||
</body>
|
||||
|
||||
@@ -3,31 +3,36 @@
|
||||
@using TaxBaik.Web.Components.Admin.Shared
|
||||
|
||||
<MudForm @ref="form">
|
||||
<MudTextField @bind-Value="model.Name" Label="이름"
|
||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
||||
<AdminFormSection Title="연락처" Description="고객 식별과 기본 회신 정보입니다." CssClass="mb-4">
|
||||
<MudTextField @bind-Value="model.Name" Label="이름"
|
||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
||||
|
||||
<MudTextField @bind-Value="model.Phone" Label="전화번호 (예: 010-1234-5678)"
|
||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
||||
<MudTextField @bind-Value="model.Phone" Label="전화번호 (예: 010-1234-5678)"
|
||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
||||
|
||||
<MudTextField @bind-Value="model.Email" Label="이메일"
|
||||
Variant="Variant.Outlined" Class="mb-4" InputType="InputType.Email" />
|
||||
<MudTextField @bind-Value="model.Email" Label="이메일"
|
||||
Variant="Variant.Outlined" Class="mb-4" InputType="InputType.Email" />
|
||||
</AdminFormSection>
|
||||
|
||||
<CommonCodeSelect @bind-Value="model.ServiceType" Group="INQUIRY_SERVICE_TYPE" Label="문의 유형" Class="mb-4" />
|
||||
<AdminFormSection Title="문의 내용" Description="운영 분류와 처리 메모를 함께 관리합니다." CssClass="mb-4">
|
||||
<CommonCodeSelect @bind-Value="model.ServiceType" Group="INQUIRY_SERVICE_TYPE" Label="문의 유형" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="model.Message" Label="문의 내용"
|
||||
Variant="Variant.Outlined" Lines="5" Class="mb-4" Required="true" />
|
||||
<MudTextField @bind-Value="model.Message" Label="문의 내용"
|
||||
Variant="Variant.Outlined" Lines="5" Class="mb-4" Required="true" />
|
||||
|
||||
<CommonCodeSelect @bind-Value="model.Status" Group="INQUIRY_STATUS" Label="상태" Class="mb-4" />
|
||||
<CommonCodeSelect @bind-Value="model.Status" Group="INQUIRY_STATUS" Label="상태" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="model.AdminMemo" Label="관리 메모"
|
||||
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
||||
<MudTextField @bind-Value="model.AdminMemo" Label="관리 메모"
|
||||
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
||||
</AdminFormSection>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" @onclick="HandleSubmit">
|
||||
@ButtonText
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" @onclick="OnCancel">취소</MudButton>
|
||||
</div>
|
||||
<AdminFormActions SubmitText="@ButtonText"
|
||||
LoadingText="저장 중..."
|
||||
CancelText="취소"
|
||||
SubmitIcon="@Icons.Material.Filled.Save"
|
||||
OnSubmit="@HandleSubmit"
|
||||
OnCancel="@OnCancel"
|
||||
IsSubmitting="false" />
|
||||
</MudForm>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<AdminTelemetryContext />
|
||||
|
||||
@Body
|
||||
|
||||
@@ -1,141 +1,7 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
@inject VersionInfo VersionInfo
|
||||
@implements IDisposable
|
||||
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout Class="admin-shell">
|
||||
<MudAppBar Elevation="0" Class="admin-topbar">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu"
|
||||
Color="Color.Inherit"
|
||||
Edge="Edge.Start"
|
||||
Class="admin-menu-button"
|
||||
OnClick="@ToggleDrawer" />
|
||||
<div class="admin-topbar-title" style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudText Typo="Typo.body2" Class="font-weight-bold" Style="color: var(--primary-color);">[TaxBaik]</MudText>
|
||||
<MudText Typo="Typo.body2" Style="font-weight: bold; color: #1E293B;">세무회계 관리 대시보드</MudText>
|
||||
</div>
|
||||
<MudSpacer />
|
||||
|
||||
<!-- 상단 액션 바 -->
|
||||
<div class="admin-topbar-actions">
|
||||
<MudTooltip Text="공개 웹사이트 방문">
|
||||
<MudButton Class="admin-topbar-action"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Inherit"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.OpenInNew"
|
||||
Href="/taxbaik"
|
||||
Target="_blank">
|
||||
공개 사이트
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
|
||||
<MudDivider Vertical="true" FlexItem="true" Class="mx-2" />
|
||||
|
||||
<MudTooltip Text="로그아웃 (Ctrl+Q)">
|
||||
<MudButton Class="admin-topbar-action"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Logout"
|
||||
Href="/taxbaik/admin/logout">
|
||||
로그아웃
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</MudAppBar>
|
||||
|
||||
<MudDrawer @bind-open="@drawerOpen"
|
||||
Elevation="0"
|
||||
Variant="DrawerVariant.Responsive"
|
||||
Breakpoint="Breakpoint.Md"
|
||||
Class="admin-drawer">
|
||||
<div class="admin-drawer-brand">
|
||||
<div class="admin-brand-mark">T</div>
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle1">TaxBaik</MudText>
|
||||
<MudText Typo="Typo.caption">세무 운영 콘솔</MudText>
|
||||
</div>
|
||||
</div>
|
||||
<MudNavMenu Class="admin-nav">
|
||||
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">대시보드</MudNavLink>
|
||||
|
||||
<MudNavGroup Title="CRM & 세무관리" Icon="@Icons.Material.Filled.BusinessCenter" @bind-Expanded="@expandedCRMGroup">
|
||||
<MudNavLink Href="/taxbaik/admin/tax-profiles" Icon="@Icons.Material.Filled.Assignment">세무 프로필</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-filing-schedules" Icon="@Icons.Material.Filled.CalendarMonth">신고 일정</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/contracts" Icon="@Icons.Material.Filled.Description">계약 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/consulting-activities" Icon="@Icons.Material.Filled.ChatBubble">상담 활동</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/revenue-trackings" Icon="@Icons.Material.Filled.Receipt">수익 추적</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavGroup Title="고객 관리" Icon="@Icons.Material.Filled.PeopleAlt" @bind-Expanded="@expandedCustomerGroup">
|
||||
<MudNavLink Href="/taxbaik/admin/clients" Icon="@Icons.Material.Filled.ContactPage">고객 카드</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-filings" Icon="@Icons.Material.Filled.Assessment">세무신고</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavGroup Title="홈페이지" Icon="@Icons.Material.Filled.Home" @bind-Expanded="@expandedWebsiteGroup">
|
||||
<MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/faqs" Icon="@Icons.Material.Filled.QuestionAnswer">FAQ 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/season-simulator" Icon="@Icons.Material.Filled.Preview">시즌 시뮬레이터</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/common-codes" Icon="@Icons.Material.Filled.Category">공통관리</MudNavLink>
|
||||
</MudNavMenu>
|
||||
|
||||
<div class="admin-drawer-version">
|
||||
<div class="admin-drawer-version-label">Version</div>
|
||||
<div class="admin-drawer-version-value">v@(VersionInfo.Version)</div>
|
||||
<div class="admin-drawer-version-built">@VersionInfo.Built</div>
|
||||
</div>
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent Class="admin-main">
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="admin-content">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
private bool drawerOpen = true;
|
||||
private bool expandedCRMGroup = true;
|
||||
private bool expandedCustomerGroup = false;
|
||||
private bool expandedWebsiteGroup = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("taxbaikAdminSession.hideLoading");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
_ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.hideLoading"));
|
||||
}
|
||||
|
||||
private void ToggleDrawer()
|
||||
{
|
||||
drawerOpen = !drawerOpen;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Navigation.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
}
|
||||
<AdminShell>
|
||||
<AdminTelemetryContext />
|
||||
@Body
|
||||
</AdminShell>
|
||||
|
||||
@@ -27,10 +27,9 @@
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||
</div>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminDataPanel Loading="@(announcements is null)" SkeletonContent="@AnnouncementSkeleton">
|
||||
@if (announcements is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else if (!FilteredAnnouncements.Any())
|
||||
{
|
||||
@@ -98,7 +97,7 @@
|
||||
검색 결과 @(FilteredAnnouncements.Count())개 · 총 @(announcements.Count)개
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -107,6 +106,14 @@
|
||||
private List<Announcement>? announcements;
|
||||
private string searchQuery = "";
|
||||
|
||||
private RenderFragment AnnouncementSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
private IEnumerable<Announcement> FilteredAnnouncements => announcements?
|
||||
.Where(a => string.IsNullOrEmpty(searchQuery) ||
|
||||
a.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -9,18 +9,15 @@
|
||||
|
||||
<PageTitle>새 포스트 작성</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Content</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">새 포스트 작성</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">새로운 블로그 포스트를 작성합니다.</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<BlogForm Model="model" Categories="categories" SubmitText="저장" OnSubmit="SavePost" OnCancel="GoBack" />
|
||||
</MudPaper>
|
||||
<AdminCrudPageShell Title="새 포스트 작성"
|
||||
Eyebrow="Content"
|
||||
Subtitle="새로운 블로그 포스트를 작성합니다."
|
||||
Loading="@false"
|
||||
OnCancel="@GoBack">
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<BlogForm Model="model" Categories="categories" SubmitText="저장" OnSubmit="SavePost" OnCancel="GoBack" />
|
||||
</MudPaper>
|
||||
</AdminCrudPageShell>
|
||||
|
||||
@code {
|
||||
private IReadOnlyList<Domain.Entities.Category> categories = [];
|
||||
|
||||
@@ -10,20 +10,13 @@
|
||||
|
||||
<PageTitle>포스트 수정</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Content</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">포스트 수정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">블로그 포스트를 수정합니다.</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (post == null)
|
||||
<AdminCrudPageShell Title="포스트 수정"
|
||||
Eyebrow="Content"
|
||||
Subtitle="블로그 포스트를 수정합니다."
|
||||
Loading="@isLoading"
|
||||
SkeletonContent="@EditorSkeleton"
|
||||
OnCancel="@GoBack">
|
||||
@if (post == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">포스트를 찾을 수 없습니다.</MudAlert>
|
||||
}
|
||||
@@ -36,6 +29,7 @@ else
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
</AdminCrudPageShell>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -46,6 +40,14 @@ else
|
||||
private BlogForm.BlogFormModel model = new();
|
||||
private bool isLoading = true;
|
||||
|
||||
private RenderFragment EditorSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 3);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -2,39 +2,43 @@
|
||||
@using TaxBaik.Domain.Entities
|
||||
|
||||
<MudForm @ref="form">
|
||||
<MudTextField @bind-Value="Model.Title" Label="제목 *"
|
||||
Variant="Variant.Outlined" Class="mb-4" Required="true" RequiredError="제목을 입력하세요." Counter="100" MaxLength="100" />
|
||||
<AdminFormSection Title="기본 정보" Description="제목과 카테고리, 발행 여부를 먼저 설정합니다." CssClass="mb-4">
|
||||
<MudTextField @bind-Value="Model.Title" Label="제목 *"
|
||||
Variant="Variant.Outlined" Class="mb-4" Required="true" RequiredError="제목을 입력하세요." Counter="100" MaxLength="100" />
|
||||
|
||||
<MudSelect T="int?" @bind-Value="Model.CategoryId" Label="카테고리"
|
||||
Variant="Variant.Outlined" Class="mb-4">
|
||||
@foreach (var category in Categories)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@((int?)category.Id)">@category.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect T="int?" @bind-Value="Model.CategoryId" Label="카테고리"
|
||||
Variant="Variant.Outlined" Class="mb-4">
|
||||
@foreach (var category in Categories)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@((int?)category.Id)">@category.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudTextField @bind-Value="Model.Content" Label="본문 내용 *"
|
||||
Variant="Variant.Outlined" Lines="16" Required="true" RequiredError="본문 내용을 입력하세요."
|
||||
Class="mb-4" />
|
||||
<MudCheckBox @bind-Checked="Model.IsPublished" Label="즉시 발행" Class="mb-4" />
|
||||
</AdminFormSection>
|
||||
|
||||
<MudTextField @bind-Value="Model.Tags" Label="태그 (쉼표로 구분)"
|
||||
Variant="Variant.Outlined" Class="mb-4" />
|
||||
<AdminFormSection Title="본문" Description="SEO와 실제 노출 본문을 함께 관리합니다." CssClass="mb-4">
|
||||
<MudTextField @bind-Value="Model.Content" Label="본문 내용 *"
|
||||
Variant="Variant.Outlined" Lines="16" Required="true" RequiredError="본문 내용을 입력하세요."
|
||||
Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="Model.SeoTitle" Label="SEO 제목"
|
||||
Variant="Variant.Outlined" Class="mb-4" />
|
||||
<MudTextField @bind-Value="Model.Tags" Label="태그 (쉼표로 구분)"
|
||||
Variant="Variant.Outlined" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="Model.SeoDescription" Label="SEO 설명"
|
||||
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
||||
<MudTextField @bind-Value="Model.SeoTitle" Label="SEO 제목"
|
||||
Variant="Variant.Outlined" Class="mb-4" />
|
||||
|
||||
<MudCheckBox @bind-Checked="Model.IsPublished" Label="즉시 발행" Class="mb-4" />
|
||||
<MudTextField @bind-Value="Model.SeoDescription" Label="SEO 설명"
|
||||
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
||||
</AdminFormSection>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" @onclick="HandleSubmit">@SubmitText</MudButton>
|
||||
@if (OnCancel.HasDelegate)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" @onclick="OnCancel">취소</MudButton>
|
||||
}
|
||||
</div>
|
||||
<AdminFormActions SubmitText="@SubmitText"
|
||||
LoadingText="저장 중..."
|
||||
CancelText="취소"
|
||||
SubmitIcon="@Icons.Material.Filled.Save"
|
||||
OnSubmit="@HandleSubmit"
|
||||
OnCancel="@OnCancel"
|
||||
IsSubmitting="false" />
|
||||
</MudForm>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -25,47 +25,47 @@
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||
</div>
|
||||
|
||||
<MudPaper Class="admin-surface mb-4" Elevation="0">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<AdminDataPanel Loading="@isLoading">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
|
||||
<MudText Typo="Typo.subtitle1">@($"검색 결과 {FilteredPosts.Count()}개 / 전체 포스트 {totalPosts}개")</MudText>
|
||||
<MudText Typo="Typo.body2">페이지 @currentPage / @totalPages</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudDataGrid Items="@FilteredPosts" Striped="true" Hoverable="true" Loading="@isLoading" Class="admin-grid">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Title" Title="제목" />
|
||||
<PropertyColumn Property="x => x.IsPublished" Title="발행">
|
||||
<CellTemplate Context="cell">
|
||||
<MudCheckBox T="bool" Value="@cell.Item.IsPublished"
|
||||
ValueChanged="@(async (bool value) => await TogglePublish(cell.Item, value))" />
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.ViewCount" Title="조회수" />
|
||||
<PropertyColumn Property="x => x.CreatedAt" Title="작성일" Format="yyyy-MM-dd" />
|
||||
<TemplateColumn>
|
||||
<CellTemplate Context="cell">
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Primary"
|
||||
Href="@($"/taxbaik/admin/blog/{cell.Item.Id}/edit")">수정하기</MudButton>
|
||||
@if (showArchived)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Success"
|
||||
@onclick="@(async () => await RestorePost(cell.Item.Id))">복원</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Error"
|
||||
@onclick="@(async () => await DeletePost(cell.Item.Id))">삭제</MudButton>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
<MudDataGrid Items="@FilteredPosts" Striped="true" Hoverable="true" Class="admin-grid">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Title" Title="제목" />
|
||||
<PropertyColumn Property="x => x.IsPublished" Title="발행">
|
||||
<CellTemplate Context="cell">
|
||||
<MudCheckBox T="bool" Value="@cell.Item.IsPublished"
|
||||
ValueChanged="@(async (bool value) => await TogglePublish(cell.Item, value))" />
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.ViewCount" Title="조회수" />
|
||||
<PropertyColumn Property="x => x.CreatedAt" Title="작성일" Format="yyyy-MM-dd" />
|
||||
<TemplateColumn>
|
||||
<CellTemplate Context="cell">
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Primary"
|
||||
Href="@($"/taxbaik/admin/blog/{cell.Item.Id}/edit")">수정하기</MudButton>
|
||||
@if (showArchived)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Success"
|
||||
@onclick="@(async () => await RestorePost(cell.Item.Id))">복원</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Error"
|
||||
@onclick="@(async () => await DeletePost(cell.Item.Id))">삭제</MudButton>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.Center" Class="mt-4" Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined" Disabled="@(currentPage <= 1 || isLoading)" @onclick="PreviousPage">이전</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Disabled="@(currentPage >= totalPages || isLoading)" @onclick="NextPage">다음</MudButton>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.Center" Class="mt-4" Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined" Disabled="@(currentPage <= 1 || isLoading)" @onclick="PreviousPage">이전</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Disabled="@(currentPage >= totalPages || isLoading)" @onclick="NextPage">다음</MudButton>
|
||||
</MudStack>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
|
||||
</section>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0" Style="max-width:720px;">
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@ClientEditSkeleton">
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -92,7 +91,7 @@
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
@@ -102,6 +101,14 @@
|
||||
private bool isValid;
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
|
||||
private RenderFragment ClientEditSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 6);
|
||||
builder.AddAttribute(2, "Columns", 3);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
|
||||
@@ -39,10 +39,9 @@
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminDataPanel Loading="@(clients is null)" SkeletonContent="@ClientListSkeleton">
|
||||
@if (clients is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else if (!clients.Any())
|
||||
{
|
||||
@@ -116,7 +115,7 @@
|
||||
}
|
||||
<MudText Typo="Typo.caption" Class="pa-2 text-muted">총 @(totalCount)명</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -130,6 +129,14 @@
|
||||
private int totalPages;
|
||||
private const int PageSize = 20;
|
||||
|
||||
private RenderFragment ClientListSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 5);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
|
||||
@@ -7,73 +7,26 @@
|
||||
|
||||
<PageTitle>공통관리</PageTitle>
|
||||
|
||||
<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>
|
||||
<AdminPageHeader Title="공통관리" Eyebrow="System" Subtitle="공통코드 그룹과 항목을 일관된 기준으로 관리합니다." />
|
||||
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem XS="12" MD="4">
|
||||
<MudPaper Class="admin-surface pa-4" Elevation="0">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">그룹</MudText>
|
||||
<MudSelect T="string" Value="@selectedGroup" ValueChanged="OnGroupChanged" Label="코드 그룹" Variant="Variant.Outlined" FullWidth="true">
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<MudSelectItem Value="@group">@group</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Class="mt-3" Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate">새 코드 추가</MudButton>
|
||||
</MudPaper>
|
||||
<CommonCodeGroupPanel Groups="groups"
|
||||
SelectedGroup="selectedGroup"
|
||||
SelectedGroupChanged="OnGroupChanged"
|
||||
OnCreateRequested="PrepareCreate" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem XS="12" MD="8">
|
||||
<MudPaper Class="admin-surface pa-4" Elevation="0">
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="@codes" Dense="true" Hover="true">
|
||||
<HeaderContent>
|
||||
<MudTh>그룹</MudTh>
|
||||
<MudTh>값</MudTh>
|
||||
<MudTh>이름</MudTh>
|
||||
<MudTh>순서</MudTh>
|
||||
<MudTh>상태</MudTh>
|
||||
<MudTh>작업</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.CodeGroup</MudTd>
|
||||
<MudTd>@context.CodeValue</MudTd>
|
||||
<MudTd>@context.CodeName</MudTd>
|
||||
<MudTd>@context.SortOrder</MudTd>
|
||||
<MudTd>@(context.IsActive ? "활성" : "비활성")</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Text" OnClick="@(() => EditCode(context))">수정</MudButton>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Error" OnClick="@(() => DeleteCode(context))">삭제</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
<MudForm @ref="form">
|
||||
<MudTextField @bind-Value="editModel.CodeGroup" Label="그룹" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!isCreateMode)" Class="mb-3" />
|
||||
<MudTextField @bind-Value="editModel.CodeValue" Label="값" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!isCreateMode)" Class="mb-3" />
|
||||
<MudTextField @bind-Value="editModel.CodeName" Label="이름" Variant="Variant.Outlined" FullWidth="true" Required="true" Class="mb-3" />
|
||||
<MudNumericField T="int" @bind-Value="editModel.SortOrder" Label="순서" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
|
||||
<MudSwitch @bind-Checked="editModel.IsActive" Color="Color.Primary">활성</MudSwitch>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveCode">저장</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="PrepareCreate">초기화</MudButton>
|
||||
</div>
|
||||
</MudForm>
|
||||
}
|
||||
</MudPaper>
|
||||
<CommonCodeListPanel Loading="@isLoading"
|
||||
Codes="codes"
|
||||
EditModel="editModel"
|
||||
IsCreateMode="isCreateMode"
|
||||
Form="form"
|
||||
EditRequested="EditCode"
|
||||
DeleteRequested="DeleteCode"
|
||||
SaveRequested="SaveCode"
|
||||
ResetRequested="PrepareCreate" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@@ -82,7 +35,6 @@
|
||||
private List<CommonCode> codes = [];
|
||||
private string selectedGroup = "";
|
||||
private bool isLoading = true;
|
||||
private MudForm? form;
|
||||
private CommonCode editModel = new();
|
||||
private bool isCreateMode = true;
|
||||
|
||||
@@ -135,16 +87,6 @@
|
||||
|
||||
private async Task SaveCode()
|
||||
{
|
||||
if (form != null)
|
||||
{
|
||||
await form.Validate();
|
||||
if (!form.IsValid)
|
||||
{
|
||||
Snackbar.Add("필수 항목을 입력하세요.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (editModel.CodeValue.Contains(' '))
|
||||
{
|
||||
Snackbar.Add("code_value에는 공백을 넣을 수 없습니다.", Severity.Error);
|
||||
|
||||
@@ -17,26 +17,24 @@
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (formModel == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">고객사를 찾을 수 없습니다.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<CompanyForm ButtonText="수정" InitialData="formModel" OnSubmit="HandleUpdate" OnCancel="GoBack" />
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@CompanySkeleton">
|
||||
@if (formModel == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">고객사를 찾을 수 없습니다.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<CompanyForm ButtonText="수정" InitialData="formModel" OnSubmit="HandleUpdate" OnCancel="GoBack" />
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Error" @onclick="DeleteCompany" Class="mt-2">
|
||||
고객사 삭제
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Error" @onclick="DeleteCompany" Class="mt-2">
|
||||
고객사 삭제
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
}
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -45,6 +43,14 @@ else
|
||||
private CompanyForm.CompanyFormModel? formModel;
|
||||
private bool isLoading = true;
|
||||
|
||||
private RenderFragment CompanySkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 6);
|
||||
builder.AddAttribute(2, "Columns", 3);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminDataPanel Loading="@(activities is null)" SkeletonContent="@ActivitySkeleton">
|
||||
@if (activities is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else if (activities.Count == 0)
|
||||
{
|
||||
@@ -90,7 +89,7 @@
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
||||
<TitleContent>
|
||||
@@ -128,6 +127,14 @@
|
||||
private ConsultingActivity? editingActivity;
|
||||
private ConsultingActivityForm activityForm = new();
|
||||
|
||||
private RenderFragment ActivitySkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<AdminEditorPanel Loading="@(contracts is null)" SkeletonContent="@ContractSkeleton">
|
||||
@if (contracts is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -142,6 +142,7 @@ else
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -156,6 +157,14 @@ else
|
||||
private Contract? selectedContract;
|
||||
private ContractForm contractForm = new();
|
||||
|
||||
private RenderFragment ContractSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 6);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
|
||||
@@ -22,151 +22,109 @@
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4">@errorMessage</MudAlert>
|
||||
}
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
|
||||
}
|
||||
|
||||
<!-- Metrics Grid -->
|
||||
<div class="admin-metric-grid">
|
||||
<div class="admin-metric-card accent-blue cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries"))'>
|
||||
<div class="admin-metric-card-body">
|
||||
<span class="admin-metric-card-label">이번달 문의</span>
|
||||
<div class="admin-metric-card-value-row">
|
||||
<span class="admin-metric-card-value" style="color: var(--primary-dark);">@summary.ThisMonthInquiries</span>
|
||||
<span class="admin-metric-card-icon" style="color: var(--primary-color);">💬</span>
|
||||
</div>
|
||||
<span class="admin-metric-card-caption">월간 상담 유입 (클릭 시 이동)</span>
|
||||
</div>
|
||||
<AdminDataPanel Loading="@isLoading" SkeletonContent="@DashboardSkeleton">
|
||||
<div class="admin-metric-grid">
|
||||
<AdminMetricCard Label="이번달 문의" Value="@summary.ThisMonthInquiries" Caption="월간 상담 유입 (클릭 시 이동)" Accent="accent-blue" Icon="💬" ValueColor="var(--primary-dark)" IconColor="var(--primary-color)" OnClick="@GoInquiries" />
|
||||
<AdminMetricCard Label="신규 문의" Value="@summary.NewInquiries" Caption="처리 대기 (클릭 시 이동)" Accent="accent-amber" Icon="⚠️" ValueColor="var(--tertiary-dark)" IconColor="var(--tertiary-color)" OnClick="@GoNewInquiries" />
|
||||
<AdminMetricCard Label="전체 포스트" Value="@summary.TotalPosts" Caption="콘텐츠 자산 (클릭 시 이동)" Accent="accent-slate" Icon="📄" ValueColor="#455a64" IconColor="#607d8b" OnClick="@GoBlog" />
|
||||
<AdminMetricCard Label="발행된 포스트" Value="@summary.PublishedPosts" Caption="검색 노출 대상 (클릭 시 이동)" Accent="accent-green" Icon="🌐" ValueColor="var(--secondary-dark)" IconColor="var(--secondary-color)" OnClick="@GoBlog" />
|
||||
</div>
|
||||
|
||||
<div class="admin-metric-card accent-amber cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries?status=new"))'>
|
||||
<div class="admin-metric-card-body">
|
||||
<span class="admin-metric-card-label">신규 문의</span>
|
||||
<div class="admin-metric-card-value-row">
|
||||
<span class="admin-metric-card-value" style="color: var(--tertiary-dark);">@summary.NewInquiries</span>
|
||||
<span class="admin-metric-card-icon" style="color: var(--tertiary-color);">⚠️</span>
|
||||
@if (upcomingFilings.Count > 0)
|
||||
{
|
||||
<MudPaper Class="admin-surface mt-4" Elevation="0">
|
||||
<div class="admin-section-header">
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">이번 달 마감 임박 신고</MudText>
|
||||
<MudText Typo="Typo.body2">30일 이내 신고 예정 건 (고객명 클릭 시 상세 카드로 연결)</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/taxbaik/admin/tax-filings">전체 일정 보기</MudButton>
|
||||
</div>
|
||||
<span class="admin-metric-card-caption">처리 대기 (클릭 시 이동)</span>
|
||||
</div>
|
||||
</div>
|
||||
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>고객</th>
|
||||
<th>신고 유형</th>
|
||||
<th>기한</th>
|
||||
<th>D-day</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var f in upcomingFilings)
|
||||
{
|
||||
var dday = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(f.DueDate));
|
||||
var effectiveDueDate = BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(f.DueDate));
|
||||
<tr>
|
||||
<td>
|
||||
<MudLink Href="@($"/taxbaik/admin/clients/{f.ClientId}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
||||
@f.ClientName
|
||||
</MudLink>
|
||||
</td>
|
||||
<td>@f.FilingType</td>
|
||||
<td>@effectiveDueDate.ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")</td>
|
||||
<td>
|
||||
@if (dday < 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Dark">기한 초과 (@(-dday)일)</MudChip>
|
||||
}
|
||||
else if (dday <= 7)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Error">D-@dday</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>D-@dday</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<div class="admin-metric-card accent-slate cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))'>
|
||||
<div class="admin-metric-card-body">
|
||||
<span class="admin-metric-card-label">전체 포스트</span>
|
||||
<div class="admin-metric-card-value-row">
|
||||
<span class="admin-metric-card-value" style="color: #455a64;">@summary.TotalPosts</span>
|
||||
<span class="admin-metric-card-icon" style="color: #607d8b;">📄</span>
|
||||
</div>
|
||||
<span class="admin-metric-card-caption">콘텐츠 자산 (클릭 시 이동)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-metric-card accent-green cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))'>
|
||||
<div class="admin-metric-card-body">
|
||||
<span class="admin-metric-card-label">발행된 포스트</span>
|
||||
<div class="admin-metric-card-value-row">
|
||||
<span class="admin-metric-card-value" style="color: var(--secondary-dark);">@summary.PublishedPosts</span>
|
||||
<span class="admin-metric-card-icon" style="color: var(--secondary-color);">🌐</span>
|
||||
</div>
|
||||
<span class="admin-metric-card-caption">검색 노출 대상 (클릭 시 이동)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (upcomingFilings.Count > 0)
|
||||
{
|
||||
<MudPaper Class="admin-surface mt-4" Elevation="0">
|
||||
<div class="admin-section-header">
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">이번 달 마감 임박 신고</MudText>
|
||||
<MudText Typo="Typo.body2">30일 이내 신고 예정 건 (고객명 클릭 시 상세 카드로 연결)</MudText>
|
||||
<MudText Typo="Typo.h6">최근 문의</MudText>
|
||||
<MudText Typo="Typo.body2">최근 유입된 상담 요청을 빠르게 확인합니다. (이름 클릭 시 상세 관리 화면으로 연계)</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/taxbaik/admin/tax-filings">전체 일정 보기</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/taxbaik/admin/inquiries">문의 전체 보기</MudButton>
|
||||
</div>
|
||||
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>고객</th>
|
||||
<th>신고 유형</th>
|
||||
<th>기한</th>
|
||||
<th>D-day</th>
|
||||
<th>이름</th>
|
||||
<th>전화</th>
|
||||
<th>분야</th>
|
||||
<th>상태</th>
|
||||
<th>날짜</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var f in upcomingFilings)
|
||||
@foreach (var inquiry in summary.RecentInquiries)
|
||||
{
|
||||
var dday = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(f.DueDate));
|
||||
var effectiveDueDate = BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(f.DueDate));
|
||||
<tr>
|
||||
<td>
|
||||
<MudLink Href="@($"/taxbaik/admin/clients/{f.ClientId}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
||||
@f.ClientName
|
||||
<MudLink Href="@($"/taxbaik/admin/inquiries?id={inquiry.Id}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
||||
@inquiry.Name
|
||||
</MudLink>
|
||||
</td>
|
||||
<td>@f.FilingType</td>
|
||||
<td>@effectiveDueDate.ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")</td>
|
||||
<td>@inquiry.Phone</td>
|
||||
<td>@inquiry.ServiceType</td>
|
||||
<td>
|
||||
@if (dday < 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Dark">기한 초과 (@(-dday)일)</MudChip>
|
||||
}
|
||||
else if (dday <= 7)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Error">D-@dday</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>D-@dday</span>
|
||||
}
|
||||
<MudChip T="string" Size="Size.Small" Color="@StatusColor(inquiry.Status)">
|
||||
@GetStatusLabel(inquiry.Status)
|
||||
</MudChip>
|
||||
</td>
|
||||
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudPaper Class="admin-surface mt-4" Elevation="0">
|
||||
<div class="admin-section-header">
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">최근 문의</MudText>
|
||||
<MudText Typo="Typo.body2">최근 유입된 상담 요청을 빠르게 확인합니다. (이름 클릭 시 상세 관리 화면으로 연계)</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/taxbaik/admin/inquiries">문의 전체 보기</MudButton>
|
||||
</div>
|
||||
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>이름</th>
|
||||
<th>전화</th>
|
||||
<th>분야</th>
|
||||
<th>상태</th>
|
||||
<th>날짜</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var inquiry in summary.RecentInquiries)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudLink Href="@($"/taxbaik/admin/inquiries?id={inquiry.Id}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
||||
@inquiry.Name
|
||||
</MudLink>
|
||||
</td>
|
||||
<td>@inquiry.Phone</td>
|
||||
<td>@inquiry.ServiceType</td>
|
||||
<td>
|
||||
<MudChip T="string" Size="Size.Small" Color="@StatusColor(inquiry.Status)">
|
||||
@GetStatusLabel(inquiry.Status)
|
||||
</MudChip>
|
||||
</td>
|
||||
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -177,6 +135,29 @@
|
||||
private string? errorMessage;
|
||||
private bool isLoading = true;
|
||||
|
||||
private RenderFragment DashboardSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 6);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
private void GoInquiries()
|
||||
{
|
||||
Nav.NavigateTo("/taxbaik/admin/inquiries");
|
||||
}
|
||||
|
||||
private void GoNewInquiries()
|
||||
{
|
||||
Nav.NavigateTo("/taxbaik/admin/inquiries?status=new");
|
||||
}
|
||||
|
||||
private void GoBlog()
|
||||
{
|
||||
Nav.NavigateTo("/taxbaik/admin/blog");
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
@@ -210,11 +191,11 @@
|
||||
|
||||
private static Color StatusColor(string status) => status switch
|
||||
{
|
||||
"new" => Color.Warning,
|
||||
"new" => Color.Warning,
|
||||
"consulting" => Color.Info,
|
||||
"contracted" => Color.Success,
|
||||
"rejected" => Color.Error,
|
||||
"closed" => Color.Dark,
|
||||
_ => Color.Default
|
||||
"rejected" => Color.Error,
|
||||
"closed" => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,13 +18,8 @@
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
|
||||
</section>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0" Style="max-width:720px;">
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@FaqSkeleton">
|
||||
<MudPaper Class="admin-surface" Elevation="0" Style="max-width:720px;">
|
||||
<MudForm @ref="form" @bind-IsValid="isValid">
|
||||
<MudGrid Spacing="3">
|
||||
<MudItem xs="12">
|
||||
@@ -68,8 +63,8 @@
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudPaper>
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
@@ -80,6 +75,14 @@
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
|
||||
private RenderFragment FaqSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 3);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
|
||||
@@ -27,10 +27,9 @@
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||
</div>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminDataPanel Loading="@(faqs is null)" SkeletonContent="@FaqListSkeleton">
|
||||
@if (faqs is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else if (!FilteredFaqs.Any())
|
||||
{
|
||||
@@ -101,7 +100,7 @@
|
||||
검색 결과 @(FilteredFaqs.Count())개 · 총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -110,6 +109,14 @@
|
||||
private List<Faq>? faqs;
|
||||
private string searchQuery = "";
|
||||
|
||||
private RenderFragment FaqListSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
private IEnumerable<Faq> FilteredFaqs => faqs?
|
||||
.Where(f => string.IsNullOrEmpty(searchQuery) ||
|
||||
f.Question.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
|
||||
@@ -8,18 +8,15 @@
|
||||
|
||||
<PageTitle>문의 등록</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Customer Relations</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">새 문의 등록</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 문의를 등록합니다. (전화, 오프라인 등)</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<InquiryForm ButtonText="등록" OnSubmit="HandleCreate" OnCancel="GoBack" />
|
||||
</MudPaper>
|
||||
<AdminCrudPageShell Title="새 문의 등록"
|
||||
Eyebrow="Customer Relations"
|
||||
Subtitle="고객 문의를 등록합니다. (전화, 오프라인 등)"
|
||||
Loading="@false"
|
||||
OnCancel="@GoBack">
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<InquiryForm ButtonText="등록" OnSubmit="HandleCreate" OnCancel="GoBack" />
|
||||
</MudPaper>
|
||||
</AdminCrudPageShell>
|
||||
|
||||
@code {
|
||||
private void GoBack()
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
|
||||
<MudGrid Class="mt-4">
|
||||
<MudItem xs="12" md="8">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">문의 정보</MudText>
|
||||
<AdminDetailSection Title="문의 정보">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이름</MudText>
|
||||
@@ -56,20 +55,18 @@
|
||||
<MudText>@inquiry.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm")</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</AdminDetailSection>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">담당자 메모</MudText>
|
||||
<AdminDetailSection Title="담당자 메모" CssClass="pa-4 mt-4">
|
||||
<MudTextField T="string" @bind-Value="adminMemo" Label="내부 메모 (고객에게 미노출)"
|
||||
Lines="4" Variant="Variant.Outlined" />
|
||||
<MudButton Class="mt-2" Variant="Variant.Filled" Color="Color.Primary"
|
||||
OnClick="SaveMemo">메모 저장</MudButton>
|
||||
</MudPaper>
|
||||
</AdminDetailSection>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="4">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">처리 상태</MudText>
|
||||
<AdminDetailSection Title="처리 상태">
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var (key, label) in InquiryStatusMapper.Labels)
|
||||
{
|
||||
@@ -81,28 +78,26 @@
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</AdminDetailSection>
|
||||
|
||||
@if (inquiry.ClientId == null)
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">고객 카드 생성</MudText>
|
||||
<AdminDetailSection Title="고객 카드 생성" CssClass="pa-4 mt-4">
|
||||
<MudText Typo="Typo.body2" Class="mb-3">이 문의를 고객 카드로 등록합니다.</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Success" FullWidth="true"
|
||||
OnClick="ConvertToClient">
|
||||
고객으로 등록
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
</AdminDetailSection>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">연결된 고객</MudText>
|
||||
<AdminDetailSection Title="연결된 고객" CssClass="pa-4 mt-4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||
Href="@($"/taxbaik/admin/clients/{inquiry.ClientId}")">
|
||||
고객 카드 보기
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
</AdminDetailSection>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@@ -9,20 +9,13 @@
|
||||
|
||||
<PageTitle>문의 수정</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Customer Relations</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">문의 수정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 문의 정보를 수정합니다.</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (inquiry == null)
|
||||
<AdminCrudPageShell Title="문의 수정"
|
||||
Eyebrow="Customer Relations"
|
||||
Subtitle="고객 문의 정보를 수정합니다."
|
||||
Loading="@isLoading"
|
||||
SkeletonContent="@EditorSkeleton"
|
||||
OnCancel="@GoBack">
|
||||
@if (inquiry == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">문의를 찾을 수 없습니다.</MudAlert>
|
||||
}
|
||||
@@ -38,6 +31,7 @@ else
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
}
|
||||
</AdminCrudPageShell>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -47,6 +41,14 @@ else
|
||||
private InquiryForm.InquiryFormModel? formModel;
|
||||
private bool isLoading = true;
|
||||
|
||||
private RenderFragment EditorSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 3);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -12,13 +12,7 @@
|
||||
</ChildContent>
|
||||
</AdminPageHeader>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Class="ma-4" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<AdminDataPanel Loading="@isLoading">
|
||||
<MudTabs Rounded="true" Elevation="0" Class="admin-tabs">
|
||||
<MudTabPanel Text="전체">
|
||||
<InquiryTable Inquiries="allInquiries" Status="" />
|
||||
@@ -39,8 +33,7 @@ else
|
||||
<InquiryTable Inquiries="allInquiries" Status="closed" />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
|
||||
@@ -2,77 +2,5 @@
|
||||
@layout TaxBaik.Web.Components.Admin.Layout.BlankLayout
|
||||
@attribute [AllowAnonymous]
|
||||
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||
@inject IApiClient ApiClient
|
||||
@inject ILocalStorageService LocalStorageService
|
||||
@inject IJSRuntime Js
|
||||
|
||||
<PageTitle>로그인</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Small" Class="admin-login-page d-flex align-center justify-center" Style="min-height: 100vh;">
|
||||
<MudPaper Class="pa-8" Elevation="3" Style="width: 100%; max-width: 400px;">
|
||||
<MudText Typo="Typo.h4" Class="mb-6 text-center">관리자 로그인</MudText>
|
||||
|
||||
<form id="admin-login-form">
|
||||
<input class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
||||
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
||||
placeholder="사용자명"
|
||||
autocomplete="username"
|
||||
name="username"
|
||||
value="@model.Username" />
|
||||
|
||||
<input type="password"
|
||||
class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
||||
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
||||
placeholder="비밀번호"
|
||||
autocomplete="current-password"
|
||||
name="password" />
|
||||
|
||||
<div class="mb-4">
|
||||
<input class="mud-checkbox" type="checkbox" name="rememberMe" />
|
||||
<label style="margin-left: 8px; cursor: pointer;">아이디 저장</label>
|
||||
</div>
|
||||
|
||||
<div class="mud-alert mud-alert-filled-error mb-4 login-error-message" style="display:none;">로그인 중 오류가 발생했습니다.</div>
|
||||
|
||||
<button type="submit"
|
||||
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
|
||||
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;">
|
||||
<span>로그인</span>
|
||||
</button>
|
||||
</form>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private readonly LoginModel model = new();
|
||||
private const string RememberedUsernameKey = "admin-remembered-username";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var remembered = await LocalStorageService.GetItemAsStringAsync(RememberedUsernameKey);
|
||||
if (!string.IsNullOrEmpty(remembered))
|
||||
{
|
||||
model.Username = remembered;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// LocalStorage may be unavailable during prerender.
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass");
|
||||
}
|
||||
|
||||
private class LoginModel
|
||||
{
|
||||
public string Username { get; set; } = "";
|
||||
public string Password { get; set; } = "";
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
<AdminLoginForm />
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<AdminDataPanel Loading="@(revenues is null)" SkeletonContent="@RevenueSkeleton">
|
||||
@if (revenues is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else if (revenues.Count == 0)
|
||||
{
|
||||
@@ -85,7 +84,7 @@
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</MudPaper>
|
||||
</AdminDataPanel>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
||||
@@ -124,6 +123,14 @@
|
||||
private bool isDialogOpen;
|
||||
private RevenueForm revenueForm = new();
|
||||
|
||||
private RenderFragment RevenueSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 5);
|
||||
builder.AddAttribute(2, "Columns", 5);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<AdminDataPanel Loading="@(schedules is null)" SkeletonContent="@ScheduleSkeleton">
|
||||
@if (schedules is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -165,6 +165,7 @@ else
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -177,6 +178,14 @@ else
|
||||
private bool isEditMode;
|
||||
private TaxFilingSchedule? selectedSchedule;
|
||||
private TaxFilingScheduleForm scheduleForm = new();
|
||||
|
||||
private RenderFragment ScheduleSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 6);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<AdminDataPanel Loading="@(profiles == null)" SkeletonContent="@ProfileSkeleton">
|
||||
@if (profiles == null)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -117,6 +117,7 @@ else
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
@@ -131,6 +132,14 @@ else
|
||||
private TaxProfile? selectedProfile;
|
||||
private TaxProfileForm profileForm = new();
|
||||
|
||||
private RenderFragment ProfileSkeleton => builder =>
|
||||
{
|
||||
builder.OpenComponent<AdminSkeletonRows>(0);
|
||||
builder.AddAttribute(1, "Rows", 6);
|
||||
builder.AddAttribute(2, "Columns", 4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">@Eyebrow</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">@Title</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(Subtitle))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">@Subtitle</MudText>
|
||||
}
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="OnCancel">
|
||||
@CancelText
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<AdminEditorPanel Loading="@Loading" SkeletonContent="@SkeletonContent">
|
||||
@ChildContent
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string Eyebrow { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public string? Subtitle { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string CancelText { get; set; } = "취소";
|
||||
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? SkeletonContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
@if (Loading)
|
||||
{
|
||||
@if (SkeletonContent is not null)
|
||||
{
|
||||
@SkeletonContent
|
||||
}
|
||||
else
|
||||
{
|
||||
<AdminSkeletonRows />
|
||||
}
|
||||
}
|
||||
else if (ChildContent is not null)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? SkeletonContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<MudPaper Class="@CssClass" Elevation="@Elevation">
|
||||
@if (!string.IsNullOrWhiteSpace(Title))
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-3">@Title</MudText>
|
||||
}
|
||||
@ChildContent
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string CssClass { get; set; } = "pa-4";
|
||||
|
||||
[Parameter]
|
||||
public int Elevation { get; set; } = 1;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<AdminDataPanel Loading="@Loading" SkeletonContent="@SkeletonContent">
|
||||
<div class="admin-editor-panel-shell">
|
||||
@ChildContent
|
||||
</div>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? SkeletonContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="d-flex gap-2">
|
||||
<MudButton Variant="@SubmitVariant"
|
||||
Color="@SubmitColor"
|
||||
StartIcon="@SubmitIcon"
|
||||
@onclick="OnSubmit"
|
||||
Disabled="@IsSubmitting">
|
||||
@(IsSubmitting ? LoadingText : SubmitText)
|
||||
</MudButton>
|
||||
@if (OnCancel.HasDelegate)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" @onclick="OnCancel">
|
||||
@CancelText
|
||||
</MudButton>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string SubmitText { get; set; } = "저장";
|
||||
|
||||
[Parameter]
|
||||
public string LoadingText { get; set; } = "저장 중...";
|
||||
|
||||
[Parameter]
|
||||
public string CancelText { get; set; } = "취소";
|
||||
|
||||
[Parameter]
|
||||
public Variant SubmitVariant { get; set; } = Variant.Filled;
|
||||
|
||||
[Parameter]
|
||||
public Color SubmitColor { get; set; } = Color.Primary;
|
||||
|
||||
[Parameter]
|
||||
public string? SubmitIcon { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnSubmit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsSubmitting { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="@CssClass">
|
||||
@if (!string.IsNullOrWhiteSpace(Title))
|
||||
{
|
||||
<MudText Typo="Typo.subtitle1" Class="font-weight-bold mb-1">@Title</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Description))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mb-2">@Description</MudText>
|
||||
}
|
||||
@ChildContent
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string CssClass { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@inject ILocalStorageService LocalStorageService
|
||||
@inject IJSRuntime Js
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Small" Class="admin-login-page d-flex align-center justify-center" Style="min-height: 100vh;">
|
||||
<MudPaper Class="pa-8" Elevation="3" Style="width: 100%; max-width: 400px;">
|
||||
<MudText Typo="Typo.h4" Class="mb-6 text-center">관리자 로그인</MudText>
|
||||
|
||||
<form id="admin-login-form">
|
||||
<input class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
||||
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
||||
placeholder="사용자명"
|
||||
autocomplete="username"
|
||||
name="username"
|
||||
value="@rememberedUsername" />
|
||||
|
||||
<input type="password"
|
||||
class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
||||
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
||||
placeholder="비밀번호"
|
||||
autocomplete="current-password"
|
||||
name="password" />
|
||||
|
||||
<div class="mb-4">
|
||||
<input class="mud-checkbox" type="checkbox" name="rememberMe" />
|
||||
<label style="margin-left: 8px; cursor: pointer;">아이디 저장</label>
|
||||
</div>
|
||||
|
||||
<div class="mud-alert mud-alert-filled-error mb-4 login-error-message" style="display:none;">로그인 중 오류가 발생했습니다.</div>
|
||||
|
||||
<button type="submit"
|
||||
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
|
||||
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;">
|
||||
<span>로그인</span>
|
||||
</button>
|
||||
</form>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private string rememberedUsername = "";
|
||||
private const string RememberedUsernameKey = "admin-remembered-username";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
rememberedUsername = await LocalStorageService.GetItemAsStringAsync(RememberedUsernameKey) ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
rememberedUsername = "";
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass");
|
||||
await Js.InvokeVoidAsync("taxbaikAdminSession.bindLoginForm");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="admin-metric-card @Accent cursor-pointer" @onclick="OnClick">
|
||||
<div class="admin-metric-card-body">
|
||||
<span class="admin-metric-card-label">@Label</span>
|
||||
<div class="admin-metric-card-value-row">
|
||||
<span class="admin-metric-card-value" style="color: @ValueColor;">@Value</span>
|
||||
<span class="admin-metric-card-icon" style="color: @IconColor;">@Icon</span>
|
||||
</div>
|
||||
<span class="admin-metric-card-caption">@Caption</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Label { get; set; } = "";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public object? Value { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string Caption { get; set; } = "";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string Accent { get; set; } = "";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string Icon { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public string ValueColor { get; set; } = "inherit";
|
||||
|
||||
[Parameter]
|
||||
public string IconColor { get; set; } = "inherit";
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnClick { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
@inject VersionInfo VersionInfo
|
||||
@implements IDisposable
|
||||
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout Class="admin-shell">
|
||||
<MudAppBar Elevation="0" Class="admin-topbar">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu"
|
||||
Color="Color.Inherit"
|
||||
Edge="Edge.Start"
|
||||
Class="admin-menu-button"
|
||||
OnClick="@ToggleDrawer" />
|
||||
<div class="admin-topbar-title">
|
||||
<MudText Typo="Typo.body2" Class="font-weight-bold admin-brand-text">TaxBaik</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-brand-subtitle">세무회계 관리 대시보드</MudText>
|
||||
</div>
|
||||
<MudSpacer />
|
||||
|
||||
<div class="admin-topbar-actions">
|
||||
<MudButton Class="admin-topbar-action"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Inherit"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.OpenInNew"
|
||||
Href="/taxbaik"
|
||||
Target="_blank">
|
||||
공개 사이트
|
||||
</MudButton>
|
||||
|
||||
<MudDivider Vertical="true" FlexItem="true" Class="mx-2" />
|
||||
|
||||
<MudButton Class="admin-topbar-action"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Logout"
|
||||
Href="/taxbaik/admin/logout">
|
||||
로그아웃
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudAppBar>
|
||||
|
||||
<MudDrawer @bind-open="@drawerOpen"
|
||||
Elevation="0"
|
||||
Variant="DrawerVariant.Responsive"
|
||||
Breakpoint="Breakpoint.Md"
|
||||
Class="admin-drawer">
|
||||
<div class="admin-drawer-brand">
|
||||
<div class="admin-brand-mark">T</div>
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle1">TaxBaik</MudText>
|
||||
<MudText Typo="Typo.caption">세무 운영 콘솔</MudText>
|
||||
</div>
|
||||
</div>
|
||||
<MudNavMenu Class="admin-nav">
|
||||
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">대시보드</MudNavLink>
|
||||
<MudNavGroup Title="CRM & 세무관리" Icon="@Icons.Material.Filled.BusinessCenter" @bind-Expanded="@expandedCRMGroup">
|
||||
<MudNavLink Href="/taxbaik/admin/tax-profiles" Icon="@Icons.Material.Filled.Assignment">세무 프로필</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-filing-schedules" Icon="@Icons.Material.Filled.CalendarMonth">신고 일정</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/contracts" Icon="@Icons.Material.Filled.Description">계약 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/consulting-activities" Icon="@Icons.Material.Filled.ChatBubble">상담 활동</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/revenue-trackings" Icon="@Icons.Material.Filled.Receipt">수익 추적</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="고객 관리" Icon="@Icons.Material.Filled.PeopleAlt" @bind-Expanded="@expandedCustomerGroup">
|
||||
<MudNavLink Href="/taxbaik/admin/clients" Icon="@Icons.Material.Filled.ContactPage">고객 카드</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/tax-filings" Icon="@Icons.Material.Filled.Assessment">세무신고</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="홈페이지" Icon="@Icons.Material.Filled.Home" @bind-Expanded="@expandedWebsiteGroup">
|
||||
<MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/faqs" Icon="@Icons.Material.Filled.QuestionAnswer">FAQ 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/season-simulator" Icon="@Icons.Material.Filled.Preview">시즌 시뮬레이터</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/common-codes" Icon="@Icons.Material.Filled.Category">공통관리</MudNavLink>
|
||||
</MudNavMenu>
|
||||
<div class="admin-drawer-version">
|
||||
<div class="admin-drawer-version-label">Version</div>
|
||||
<div class="admin-drawer-version-value">v@(VersionInfo.Version)</div>
|
||||
<div class="admin-drawer-version-built">@VersionInfo.Built</div>
|
||||
</div>
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent Class="admin-main">
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="admin-content">
|
||||
@ChildContent
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private bool drawerOpen = true;
|
||||
private bool expandedCRMGroup = true;
|
||||
private bool expandedCustomerGroup = false;
|
||||
private bool expandedWebsiteGroup = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("taxbaikAdminSession.setContext", "admin/shell", "navigation", "layout", "shell", "shell", "", "main");
|
||||
await JS.InvokeVoidAsync("taxbaikAdminSession.hideLoading");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
var route = new Uri(args.Location).AbsolutePath;
|
||||
_ = JS.InvokeVoidAsync("taxbaikAdminSession.setContext", route, "navigation", "route-change", "layout", "shell", "", route);
|
||||
_ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.hideLoading"));
|
||||
}
|
||||
|
||||
private void ToggleDrawer()
|
||||
{
|
||||
drawerOpen = !drawerOpen;
|
||||
_ = JS.InvokeVoidAsync("taxbaikAdminSession.setContext", "admin/shell", "navigation", "drawer", drawerOpen ? "opened" : "closed", "shell", "", "drawer");
|
||||
_ = JS.InvokeVoidAsync("taxbaikAdminSession.traceUiState", "admin-shell", drawerOpen ? "drawer opened" : "drawer closed");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Navigation.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="admin-skeleton-stack">
|
||||
@for (var i = 0; i < Rows; i++)
|
||||
{
|
||||
<div class="admin-skeleton-row">
|
||||
@for (var j = 0; j < Columns; j++)
|
||||
{
|
||||
<div class="admin-skeleton-block @GetWidthClass(j)" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Rows { get; set; } = 4;
|
||||
|
||||
[Parameter]
|
||||
public int Columns { get; set; } = 3;
|
||||
|
||||
private static string GetWidthClass(int index) => index switch
|
||||
{
|
||||
0 => "w-40",
|
||||
1 => "w-25",
|
||||
_ => "w-20"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
@using System.Text.RegularExpressions
|
||||
@inject IJSRuntime Js
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
[Parameter] public string Screen { get; set; } = "";
|
||||
[Parameter] public string Feature { get; set; } = "";
|
||||
[Parameter] public string Action { get; set; } = "";
|
||||
[Parameter] public string Step { get; set; } = "";
|
||||
[Parameter] public string Entity { get; set; } = "";
|
||||
[Parameter] public string EntityId { get; set; } = "";
|
||||
[Parameter] public string DataKey { get; set; } = "";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var route = GetRoute();
|
||||
var context = ResolveContext(route);
|
||||
await Js.InvokeVoidAsync("taxbaikAdminSession.setContext",
|
||||
string.IsNullOrWhiteSpace(Screen) ? context.Screen : Screen,
|
||||
string.IsNullOrWhiteSpace(Feature) ? context.Feature : Feature,
|
||||
string.IsNullOrWhiteSpace(Action) ? context.Action : Action,
|
||||
string.IsNullOrWhiteSpace(Step) ? context.Step : Step,
|
||||
string.IsNullOrWhiteSpace(Entity) ? context.Entity : Entity,
|
||||
string.IsNullOrWhiteSpace(EntityId) ? context.EntityId : EntityId,
|
||||
string.IsNullOrWhiteSpace(DataKey) ? context.DataKey : DataKey);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRoute()
|
||||
{
|
||||
var path = Navigation.ToBaseRelativePath(Navigation.Uri);
|
||||
return string.IsNullOrWhiteSpace(path) ? "/" : "/" + path.TrimStart('/');
|
||||
}
|
||||
|
||||
private static (string Screen, string Feature, string Action, string Step, string Entity, string EntityId, string DataKey) ResolveContext(string route)
|
||||
=> route.ToLowerInvariant() switch
|
||||
{
|
||||
"/" => ("admin/index", "shell", "load", "index", "admin", "", "index"),
|
||||
"/admin/login" => ("admin/login", "auth", "render", "login page", "auth", "", "login"),
|
||||
"/admin/dashboard" => ("admin/dashboard", "dashboard", "load", "summary", "dashboard", "", "summary"),
|
||||
"/admin/common-codes" => ("admin/common-codes", "common-code", "load", "group list", "common_code", "", "group"),
|
||||
"/admin/blog" => ("admin/blog", "content", "load", "list", "blog", "", "list"),
|
||||
"/admin/blog/create" => ("admin/blog/create", "content", "create", "form", "blog", "", "create"),
|
||||
"/admin/blog/0/edit" => ("admin/blog/edit", "content", "edit", "form", "blog", "0", "edit"),
|
||||
"/admin/inquiries" => ("admin/inquiries", "customer-request", "load", "list", "inquiry", "", "list"),
|
||||
"/admin/inquiries/create" => ("admin/inquiries/create", "customer-request", "create", "form", "inquiry", "", "create"),
|
||||
"/admin/settings" => ("admin/settings", "system", "load", "settings", "site_setting", "", "settings"),
|
||||
"/admin/announcements" => ("admin/announcements", "content", "load", "list", "announcement", "", "list"),
|
||||
"/admin/announcements/create" => ("admin/announcements/create", "content", "create", "form", "announcement", "", "create"),
|
||||
"/admin/companies" => ("admin/companies", "company", "load", "list", "company", "", "list"),
|
||||
"/admin/faqs" => ("admin/faqs", "faq", "load", "list", "faq", "", "list"),
|
||||
"/admin/tax-profiles" => ("admin/tax-profiles", "tax-profile", "load", "list", "tax_profile", "", "list"),
|
||||
"/admin/tax-filing-schedules" => ("admin/tax-filing-schedules", "schedule", "load", "list", "tax_filing_schedule", "", "list"),
|
||||
"/admin/contracts" => ("admin/contracts", "crm", "load", "list", "contract", "", "list"),
|
||||
"/admin/consulting-activities" => ("admin/consulting-activities", "crm", "load", "list", "consulting_activity", "", "list"),
|
||||
"/admin/revenue-trackings" => ("admin/revenue-trackings", "crm", "load", "list", "revenue_tracking", "", "list"),
|
||||
"/admin/clients" => ("admin/clients", "customer", "load", "list", "client", "", "list"),
|
||||
"/admin/tax-filings" => ("admin/tax-filings", "tax-filing", "load", "list", "tax_filing", "", "list"),
|
||||
"/admin/season-simulator" => ("admin/season-simulator", "schedule", "load", "simulator", "season", "", "simulator"),
|
||||
_ => ResolveDynamicContext(route)
|
||||
};
|
||||
|
||||
private static (string Screen, string Feature, string Action, string Step, string Entity, string EntityId, string DataKey) ResolveDynamicContext(string route)
|
||||
{
|
||||
var normalized = route.ToLowerInvariant().TrimEnd('/');
|
||||
|
||||
foreach (var pattern in new[]
|
||||
{
|
||||
("/admin/blog/", "admin/blog/edit", "content", "edit", "form", "blog", "edit"),
|
||||
("/admin/announcements/", "admin/announcements/edit", "content", "edit", "form", "announcement", "edit"),
|
||||
("/admin/inquiries/", "admin/inquiries/edit", "customer-request", "edit", "form", "inquiry", "edit"),
|
||||
("/admin/clients/", "admin/clients/detail", "customer", "view", "detail", "client", "detail"),
|
||||
("/admin/companies/", "admin/companies/edit", "company", "edit", "form", "company", "edit"),
|
||||
("/admin/faqs/", "admin/faqs/edit", "faq", "edit", "form", "faq", "edit"),
|
||||
("/admin/tax-profiles/", "admin/tax-profiles/edit", "tax-profile", "edit", "form", "tax_profile", "edit"),
|
||||
("/admin/tax-filing-schedules/", "admin/tax-filing-schedules/edit", "schedule", "edit", "form", "tax_filing_schedule", "edit"),
|
||||
})
|
||||
{
|
||||
if (!normalized.StartsWith(pattern.Item1, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var remainder = normalized[pattern.Item1.Length..].Trim('/');
|
||||
var id = ExtractLeadingId(remainder);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
id = remainder.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
|
||||
|
||||
return (pattern.Item2, pattern.Item3, pattern.Item4, pattern.Item5, pattern.Item6, id, pattern.Item7);
|
||||
}
|
||||
|
||||
return (route.Trim('/'), "admin", "load", "view", "admin", "", route.Trim('/'));
|
||||
}
|
||||
|
||||
private static string ExtractLeadingId(string value)
|
||||
{
|
||||
var match = Regex.Match(value, @"^\d+");
|
||||
return match.Success ? match.Value : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<AdminDataPanel Loading="@false">
|
||||
<AdminFormSection Title="그룹 선택" Description="코드 그룹을 먼저 선택합니다." CssClass="mb-4">
|
||||
<MudSelect T="string"
|
||||
Value="@SelectedGroup"
|
||||
ValueChanged="OnSelectedGroupChanged"
|
||||
Label="코드 그룹"
|
||||
Variant="Variant.Outlined"
|
||||
FullWidth="true">
|
||||
@foreach (var group in Groups)
|
||||
{
|
||||
<MudSelectItem Value="@group">@group</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</AdminFormSection>
|
||||
|
||||
<AdminFormSection Title="새 코드" Description="선택한 그룹에 항목을 추가합니다.">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnCreateRequested">새 코드 추가</MudButton>
|
||||
</AdminFormSection>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public IReadOnlyList<string> Groups { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public string SelectedGroup { get; set; } = "";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback<string> SelectedGroupChanged { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback OnCreateRequested { get; set; }
|
||||
|
||||
private Task OnSelectedGroupChanged(string value) => SelectedGroupChanged.InvokeAsync(value);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<AdminDataPanel Loading="@Loading">
|
||||
<AdminFormSection Title="코드 목록" Description="그룹별 공통코드와 상태를 관리합니다." CssClass="mb-4">
|
||||
<MudTable Items="@Codes" Dense="true" Hover="true">
|
||||
<HeaderContent>
|
||||
<MudTh>그룹</MudTh>
|
||||
<MudTh>값</MudTh>
|
||||
<MudTh>이름</MudTh>
|
||||
<MudTh>순서</MudTh>
|
||||
<MudTh>상태</MudTh>
|
||||
<MudTh>작업</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.CodeGroup</MudTd>
|
||||
<MudTd>@context.CodeValue</MudTd>
|
||||
<MudTd>@context.CodeName</MudTd>
|
||||
<MudTd>@context.SortOrder</MudTd>
|
||||
<MudTd>@(context.IsActive ? "활성" : "비활성")</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Text" OnClick="@(async () => await InvokeEditAsync(context))">수정</MudButton>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Error" OnClick="@(async () => await InvokeDeleteAsync(context))">삭제</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</AdminFormSection>
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
<AdminFormSection Title="코드 편집" Description="공백 없는 값과 일관된 이름만 허용합니다.">
|
||||
<MudForm>
|
||||
<MudTextField @bind-Value="EditModel.CodeGroup" Label="그룹" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!IsCreateMode)" Class="mb-3" />
|
||||
<MudTextField @bind-Value="EditModel.CodeValue" Label="값" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!IsCreateMode)" Class="mb-3" />
|
||||
<MudTextField @bind-Value="EditModel.CodeName" Label="이름" Variant="Variant.Outlined" FullWidth="true" Required="true" Class="mb-3" />
|
||||
<MudNumericField T="int" @bind-Value="EditModel.SortOrder" Label="순서" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
|
||||
<MudSwitch @bind-Checked="EditModel.IsActive" Color="Color.Primary">활성</MudSwitch>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnSaveRequested">저장</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="OnResetRequested">초기화</MudButton>
|
||||
</div>
|
||||
</MudForm>
|
||||
</AdminFormSection>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public IReadOnlyList<CommonCode> Codes { get; set; } = [];
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public CommonCode EditModel { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public bool IsCreateMode { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback<CommonCode> EditRequested { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback<CommonCode> DeleteRequested { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback SaveRequested { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback ResetRequested { get; set; }
|
||||
|
||||
private Task InvokeEditAsync(CommonCode code) => EditRequested.InvokeAsync(code);
|
||||
private Task InvokeDeleteAsync(CommonCode code) => DeleteRequested.InvokeAsync(code);
|
||||
private Task OnSaveRequested() => SaveRequested.InvokeAsync();
|
||||
private Task OnResetRequested() => ResetRequested.InvokeAsync();
|
||||
}
|
||||
Reference in New Issue
Block a user