abad1630b6
TaxBaik CI/CD / build-and-deploy (push) Failing after 41s
- Add EasyMDE 2.18.0 CDN to App.razor - Add Marked.js for markdown preview rendering - Replace MudTextField with EasyMDE editor in BlogCreate.razor - Replace MudTextField with EasyMDE editor in BlogEdit.razor - Add JavaScript interop for editor initialization and content sync - Support markdown syntax highlighting and formatting toolbar Features: ✅ Bold, italic, strikethrough ✅ Headings (H1-H6) ✅ Code blocks and inline code ✅ Lists (ordered/unordered) ✅ Links and images ✅ Tables ✅ Quotes ✅ Horizontal rules ✅ Real-time preview (side-by-side mode) ✅ Full-screen editing ✅ Markdown guide The editor syncs content with Blazor form on save. Markdown syntax is preserved in database and rendered as HTML on blog pages.
242 lines
8.0 KiB
Plaintext
242 lines
8.0 KiB
Plaintext
@page "/admin/blog/{id:int}/edit"
|
|
@attribute [Authorize]
|
|
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
|
@using TaxBaik.Application.DTOs
|
|
@using TaxBaik.Application.Services
|
|
@using TaxBaik.Domain.Interfaces
|
|
@inject BlogService BlogService
|
|
@inject ICategoryRepository CategoryRepository
|
|
@inject NavigationManager Navigation
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
|
|
<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)
|
|
{
|
|
<MudAlert Severity="Severity.Error" Class="mt-4">포스트를 찾을 수 없습니다.</MudAlert>
|
|
}
|
|
else
|
|
{
|
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
|
<MudForm @ref="form">
|
|
<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>
|
|
|
|
<div class="mb-4">
|
|
<label class="d-block mb-2" style="font-weight: 500;">본문 내용 (마크다운) *</label>
|
|
<textarea id="markdown-editor" @bind="model.Content" style="display: none;"></textarea>
|
|
<div id="editor-container" style="border: 1px solid #d0d0d0; border-radius: 4px; min-height: 400px;"></div>
|
|
</div>
|
|
|
|
<MudTextField @bind-Value="model.Tags" Label="태그 (쉼표로 구분)"
|
|
Variant="Variant.Outlined" Class="mb-4" />
|
|
|
|
<MudTextField @bind-Value="model.SeoTitle" Label="SEO 제목"
|
|
Variant="Variant.Outlined" Class="mb-4" />
|
|
|
|
<MudTextField @bind-Value="model.SeoDescription" Label="SEO 설명"
|
|
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
|
|
|
<MudCheckBox @bind-Checked="model.IsPublished" Label="발행" Class="mb-4" />
|
|
|
|
<div class="d-flex gap-2">
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
|
@onclick="SavePost">저장</MudButton>
|
|
<MudButton Variant="Variant.Outlined" Color="Color.Error"
|
|
@onclick="DeletePost">삭제</MudButton>
|
|
</div>
|
|
</MudForm>
|
|
</MudPaper>
|
|
}
|
|
|
|
@code {
|
|
[Parameter]
|
|
public int Id { get; set; }
|
|
|
|
[Inject]
|
|
private IJSRuntime JS { get; set; } = null!;
|
|
|
|
private MudForm? form;
|
|
private Domain.Entities.BlogPost? post;
|
|
private List<Domain.Entities.Category> categories = [];
|
|
private EditPostModel model = new();
|
|
private bool isLoading = true;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
try
|
|
{
|
|
post = await BlogService.GetByIdAsync(Id);
|
|
if (post != null)
|
|
{
|
|
categories = (await CategoryRepository.GetAllAsync()).ToList();
|
|
MapPostToModel(post);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"포스트 로드 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && post != null)
|
|
{
|
|
await JS.InvokeVoidAsync("window.initMarkdownEditor", "markdown-editor", model.Content ?? "");
|
|
}
|
|
}
|
|
|
|
private void MapPostToModel(Domain.Entities.BlogPost post)
|
|
{
|
|
model.Title = post.Title;
|
|
model.Content = post.Content;
|
|
model.CategoryId = post.CategoryId;
|
|
model.Tags = post.Tags;
|
|
model.SeoTitle = post.SeoTitle;
|
|
model.SeoDescription = post.SeoDescription;
|
|
model.IsPublished = post.IsPublished;
|
|
}
|
|
|
|
private void GoBack()
|
|
{
|
|
Navigation.NavigateTo("/taxbaik/admin/blog");
|
|
}
|
|
|
|
private async Task SavePost()
|
|
{
|
|
if (form == null || post == null)
|
|
return;
|
|
|
|
// 에디터에서 최신 내용 가져오기
|
|
model.Content = await JS.InvokeAsync<string>("window.getMarkdownContent");
|
|
|
|
if (string.IsNullOrWhiteSpace(model.Content))
|
|
{
|
|
Snackbar.Add("본문 내용을 입력하세요.", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
await form.Validate();
|
|
if (!form.IsValid)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await BlogService.UpdateAsync(post.Id, new CreateBlogPostDto
|
|
{
|
|
Title = model.Title,
|
|
Content = model.Content,
|
|
CategoryId = model.CategoryId,
|
|
Tags = model.Tags,
|
|
SeoTitle = model.SeoTitle,
|
|
SeoDescription = model.SeoDescription,
|
|
IsPublished = model.IsPublished
|
|
});
|
|
|
|
Snackbar.Add("포스트가 저장되었습니다.", Severity.Success);
|
|
Navigation.NavigateTo("/taxbaik/admin/blog");
|
|
}
|
|
catch (ValidationException ex)
|
|
{
|
|
Snackbar.Add(ex.Message, Severity.Error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DeletePost()
|
|
{
|
|
if (post == null)
|
|
return;
|
|
|
|
var result = await DialogService.ShowMessageBox(
|
|
"포스트 삭제",
|
|
"정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
|
"삭제", "취소");
|
|
|
|
if (result != true)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await BlogService.DeleteAsync(post.Id);
|
|
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
|
Navigation.NavigateTo("/taxbaik/admin/blog");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private class EditPostModel
|
|
{
|
|
public string Title { get; set; } = "";
|
|
public string Content { get; set; } = "";
|
|
public int? CategoryId { get; set; }
|
|
public string? Tags { get; set; }
|
|
public string? SeoTitle { get; set; }
|
|
public string? SeoDescription { get; set; }
|
|
public bool IsPublished { get; set; }
|
|
}
|
|
}
|
|
|
|
<!-- EasyMDE 초기화 스크립트 -->
|
|
<script>
|
|
window.initMarkdownEditor = function(editorId, initialContent) {
|
|
if (!window.easyMDEInstance) {
|
|
window.easyMDEInstance = new EasyMDE({
|
|
element: document.getElementById(editorId),
|
|
spellChecker: false,
|
|
autoDownloadFontAwesome: false,
|
|
initialValue: initialContent || "",
|
|
toolbar: [
|
|
"bold", "italic", "strikethrough", "|",
|
|
"heading", "code", "|",
|
|
"unordered-list", "ordered-list", "|",
|
|
"link", "image", "table", "|",
|
|
"quote", "horizontal-rule", "|",
|
|
"preview", "side-by-side", "fullscreen", "|",
|
|
"guide"
|
|
],
|
|
previewRender: function(plainText) {
|
|
return marked.parse(plainText);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
window.getMarkdownContent = function() {
|
|
return window.easyMDEInstance ? window.easyMDEInstance.value() : "";
|
|
};
|
|
</script>
|