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:
@@ -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