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:
2026-07-04 04:03:18 +09:00
parent 64e462e57e
commit 54367696dc
112 changed files with 2701 additions and 207 deletions
@@ -0,0 +1,66 @@
@page "/admin/blog/create"
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
@attribute [Authorize]
@using TaxBaik.Application.DTOs
@using TaxBaik.WasmClient.Components.Admin.Pages.Blog
@inject IBlogBrowserClient BlogClient
@inject ICategoryBrowserClient CategoryClient
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<PageTitle>새 포스트 작성</PageTitle>
<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 = [];
private BlogForm.BlogFormModel model = new();
protected override async Task OnInitializedAsync()
{
categories = await CategoryClient.GetAllAsync();
}
private void GoBack()
{
Navigation.NavigateTo("/taxbaik/admin/blog");
}
private async Task SavePost()
{
try
{
var result = await BlogClient.CreateAsync(new CreateBlogPostDto
{
Title = model.Title,
Content = model.Content,
CategoryId = model.CategoryId,
Tags = model.Tags,
SeoTitle = model.SeoTitle,
SeoDescription = model.SeoDescription,
IsPublished = model.IsPublished
});
if (result == null)
{
Snackbar.Add("포스트 저장에 실패했습니다.", Severity.Error);
return;
}
Snackbar.Add("포스트가 저장되었습니다.", Severity.Success);
Navigation.NavigateTo("/taxbaik/admin/blog");
}
catch (ValidationException ex)
{
Snackbar.Add(ex.Message, Severity.Error);
}
}
}