refactor: complete WebAssembly migration - proper architecture
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m17s
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m17s
Phase 8: Complete WebAssembly 렌더 모드 전환 (정공법) Migration Summary: - ALL Admin components → TaxBaik.Web.Client - Routes.razor, Pages/*, Layout/*, Shared/*, Forms/* - App.razor → TaxBaik.WasmClient (호스트 컴포넌트) - Shared utilities → TaxBaik.Application.Utils Architecture: ✅ App.razor: TaxBaik.WasmClient (WebAssembly, 호스트) ✅ Routes + Pages: TaxBaik.WasmClient (WebAssembly) ✅ Layout + Shared + Forms: TaxBaik.WasmClient (WebAssembly) ✅ Services: TaxBaik.Web (API-First) Key Changes: - Namespaces: TaxBaik.Web.Components.Admin → TaxBaik.WasmClient.Components.Admin - Shared utilities: TaxBaik.Application.Utils (single source of truth) - Program.cs: MapRazorComponents<TaxBaik.WasmClient.Components.Admin.App>() - _Imports.razor: Components/Admin 폴더에 재구성 Build Status: ✅ 0 errors, 0 warnings Benefits: - Stateless server (no Circuit memory) - Client-side rendering (WebAssembly) - Unlimited concurrent users (horizontal scaling) - ERP-ready architecture Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
@page "/admin/blog"
|
||||
@attribute [Authorize]
|
||||
@inject IBlogBrowserClient BlogClient
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>블로그 관리</PageTitle>
|
||||
|
||||
<AdminPageHeader Title="블로그 관리" Eyebrow="Content" Subtitle="검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.">
|
||||
<ChildContent>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Restore"
|
||||
OnClick="ToggleArchiveView">
|
||||
@(showArchived ? "전체 글 보기" : "숨김 글 보기")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Refresh"
|
||||
OnClick="Reload">
|
||||
새로고침
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.EditNote"
|
||||
Href="/taxbaik/admin/blog/create">새 포스트 작성</MudButton>
|
||||
</ChildContent>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="d-flex pa-4 gap-4 align-center">
|
||||
<MudTextField @bind-Value="searchQuery" Placeholder="블로그 제목 또는 본문 검색..." Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</AdminDataPanel>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||
|
||||
private List<TaxBaik.Application.DTOs.BlogPostResponseDto> posts = [];
|
||||
private string searchQuery = "";
|
||||
private bool isLoading = true;
|
||||
private int currentPage = 1;
|
||||
private int totalPages = 1;
|
||||
private int totalPosts = 0;
|
||||
private bool showArchived;
|
||||
private const int PageSize = 20;
|
||||
|
||||
private IEnumerable<TaxBaik.Application.DTOs.BlogPostResponseDto> FilteredPosts => posts
|
||||
.Where(p => string.IsNullOrEmpty(searchQuery) ||
|
||||
p.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
(p.Content != null && p.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
{
|
||||
var authState = await AuthStateTask;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
await LoadPosts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPosts()
|
||||
{
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
var result = showArchived
|
||||
? await BlogClient.GetArchivedPagedAsync(currentPage, PageSize)
|
||||
: await BlogClient.GetAdminPagedAsync(currentPage, PageSize);
|
||||
posts = result.Items.ToList();
|
||||
totalPosts = result.Total;
|
||||
totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize));
|
||||
}
|
||||
catch
|
||||
{
|
||||
posts = [];
|
||||
totalPosts = 0;
|
||||
totalPages = 1;
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
private async Task PreviousPage()
|
||||
{
|
||||
if (currentPage <= 1)
|
||||
return;
|
||||
|
||||
currentPage--;
|
||||
await LoadPosts();
|
||||
}
|
||||
|
||||
private async Task NextPage()
|
||||
{
|
||||
if (currentPage >= totalPages)
|
||||
return;
|
||||
|
||||
currentPage++;
|
||||
await LoadPosts();
|
||||
}
|
||||
|
||||
private async Task TogglePublish(TaxBaik.Application.DTOs.BlogPostResponseDto post, bool isPublished)
|
||||
{
|
||||
var previous = post.IsPublished;
|
||||
post.IsPublished = isPublished;
|
||||
var result = await BlogClient.UpdateAsync(post.Id, new TaxBaik.Application.DTOs.CreateBlogPostDto
|
||||
{
|
||||
Title = post.Title,
|
||||
Content = post.Content,
|
||||
CategoryId = post.CategoryId,
|
||||
Tags = post.Tags,
|
||||
SeoTitle = post.SeoTitle,
|
||||
SeoDescription = post.SeoDescription,
|
||||
ThumbnailUrl = post.ThumbnailUrl,
|
||||
IsPublished = isPublished,
|
||||
AuthorId = post.AuthorId
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
post.IsPublished = previous;
|
||||
Snackbar.Add("발행 상태 변경에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("발행 상태가 변경되었습니다.", Severity.Success);
|
||||
}
|
||||
|
||||
private async Task DeletePost(int postId)
|
||||
{
|
||||
var deleted = await BlogClient.DeleteAsync(postId);
|
||||
if (!deleted)
|
||||
{
|
||||
Snackbar.Add("포스트 삭제에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
||||
await LoadPosts();
|
||||
}
|
||||
|
||||
private async Task RestorePost(int postId)
|
||||
{
|
||||
var restored = await BlogClient.RestoreAsync(postId);
|
||||
if (!restored)
|
||||
{
|
||||
Snackbar.Add("포스트 복원에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("포스트가 복원되었습니다.", Severity.Success);
|
||||
await LoadPosts();
|
||||
}
|
||||
|
||||
private async Task ToggleArchiveView()
|
||||
{
|
||||
showArchived = !showArchived;
|
||||
currentPage = 1;
|
||||
await LoadPosts();
|
||||
}
|
||||
|
||||
private async Task Reload() => await LoadPosts();
|
||||
}
|
||||
Reference in New Issue
Block a user