feat: standalone Blazor WebAssembly admin + SEO enhancements
Architecture: - Admin UI: /admin (Standalone Blazor WebAssembly, 219 WASM files) - Portal: /portal (Razor Pages, Cookie/OAuth auth) - Homepage: / (Razor Pages, SSR) - API: /api (FastEndpoints + JWT) SEO: - Sitemap: Public content only (blog, FAQ, announcements, contact) - robots.txt: Exclude /admin and /portal, reference production domain - Naver verification: naverb1813cd79ddc2ded5c5291fca5cb46c2.html ready Technical: - TaxBaik.Web.Client: StaticWebAssetBasePath=admin - Server Program.cs: UseBlazorFrameworkFiles + MapFallback for SPA routing - base href="/admin/" for client-side navigation - blazor.webassembly.js (standalone, not web.js) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
@page "/admin/clients/create"
|
||||
@page "/admin/clients/{Id:int}/edit"
|
||||
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||
@attribute [Authorize]
|
||||
@using TaxBaik.Application.DTOs
|
||||
@using TaxBaik.Web.Services
|
||||
@using TaxBaik.Domain.Entities
|
||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>@(Id.HasValue ? "고객 수정" : "고객 등록")</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "고객 수정" : "고객 등록")</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/clients"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
|
||||
</section>
|
||||
|
||||
<AdminEditorPanel Loading="@isLoading" SkeletonContent="@ClientEditSkeleton">
|
||||
@if (isLoading)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudForm @ref="form" @bind-IsValid="isValid">
|
||||
<MudGrid Spacing="3">
|
||||
@* 기본 정보 *@
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.subtitle1" Class="fw-bold mb-1">기본 정보</MudText>
|
||||
<MudDivider />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="dto.Name" Label="고객명 *" Required="true"
|
||||
RequiredError="고객명을 입력하세요." />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="dto.CompanyName" Label="회사명 (선택)" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="dto.Phone" Label="연락처"
|
||||
Placeholder="010-0000-0000" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="dto.Email" Label="이메일" InputType="InputType.Email" />
|
||||
</MudItem>
|
||||
|
||||
@* 세무 정보 *@
|
||||
<MudItem xs="12" Class="mt-2">
|
||||
<MudText Typo="Typo.subtitle1" Class="fw-bold mb-1">세무 정보</MudText>
|
||||
<MudDivider />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<CommonCodeSelect @bind-Value="dto.ServiceType" Group="CLIENT_SERVICE_TYPE" Label="서비스 유형" Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<CommonCodeSelect @bind-Value="dto.TaxType" Group="CLIENT_TAX_TYPE" Label="세금 유형" Clearable="true" />
|
||||
</MudItem>
|
||||
|
||||
@* 관리 정보 *@
|
||||
<MudItem xs="12" Class="mt-2">
|
||||
<MudText Typo="Typo.subtitle1" Class="fw-bold mb-1">관리 정보</MudText>
|
||||
<MudDivider />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<CommonCodeSelect @bind-Value="dto.Status" Group="CLIENT_STATUS" Label="상태 *" Required="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<CommonCodeSelect @bind-Value="dto.Source" Group="CLIENT_SOURCE" Label="유입 경로" Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="dto.Memo" Label="메모"
|
||||
Lines="4" AutoGrow="true"
|
||||
Placeholder="상담 배경, 특이사항, 중요 날짜 등 자유롭게 기록하세요" />
|
||||
</MudItem>
|
||||
|
||||
@* 저장 버튼 *@
|
||||
<MudItem xs="12" Class="d-flex gap-2 mt-2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Save"
|
||||
OnClick="@SaveAsync" Disabled="@isSaving">
|
||||
@(isSaving ? "저장 중..." : "저장")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/clients">
|
||||
취소
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
}
|
||||
</AdminEditorPanel>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private MudForm form = null!;
|
||||
private CreateClientDto dto = new() { Status = "active" };
|
||||
private bool isValid;
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await ClientClient.GetByIdAsync(Id.Value);
|
||||
if (client is null)
|
||||
{
|
||||
Snackbar.Add("고객을 찾을 수 없습니다.", Severity.Error);
|
||||
Navigation.NavigateTo("/taxbaik/admin/clients");
|
||||
return;
|
||||
}
|
||||
dto = new CreateClientDto
|
||||
{
|
||||
Name = client.Name,
|
||||
CompanyName = client.CompanyName,
|
||||
Phone = client.Phone,
|
||||
Email = client.Email,
|
||||
ServiceType = client.ServiceType,
|
||||
TaxType = client.TaxType,
|
||||
Status = client.Status,
|
||||
Source = client.Source,
|
||||
Memo = client.Memo
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
|
||||
Navigation.NavigateTo("/taxbaik/admin/clients");
|
||||
return;
|
||||
}
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
await form.Validate();
|
||||
if (!isValid) return;
|
||||
|
||||
isSaving = true;
|
||||
try
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
var result = await ClientClient.UpdateAsync(Id.Value, dto);
|
||||
if (result != null)
|
||||
Snackbar.Add("고객 정보가 수정되었습니다.", Severity.Success);
|
||||
else
|
||||
Snackbar.Add("수정에 실패했습니다.", Severity.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await ClientClient.CreateAsync(dto);
|
||||
if (result != null)
|
||||
Snackbar.Add("고객이 등록되었습니다.", Severity.Success);
|
||||
else
|
||||
Snackbar.Add("등록에 실패했습니다.", Severity.Error);
|
||||
}
|
||||
Navigation.NavigateTo("/taxbaik/admin/clients");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user