feat: add EasyMDE markdown editor for blog creation/editing
TaxBaik CI/CD / build-and-deploy (push) Failing after 41s
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.
This commit is contained in:
@@ -11,6 +11,11 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<!-- EasyMDE 마크다운 에디터 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde@2.18.0/dist/easymde.min.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/easymde@2.18.0/dist/easymde.min.js"></script>
|
||||
<!-- Marked 라이브러리 (EasyMDE 미리보기용) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
<script>
|
||||
document.documentElement.classList.toggle(
|
||||
'admin-login-route',
|
||||
|
||||
@@ -33,24 +33,11 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudTabs Elevation="2" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-4" Class="mb-4">
|
||||
<MudTabPanel Text="에디터" Icon="@Icons.Material.Filled.Edit">
|
||||
<MudTextField @bind-Value="model.Content" Label="본문 내용 *"
|
||||
Variant="Variant.Outlined" Lines="15" Required="true" RequiredError="본문 내용을 입력하세요." Counter="10000" MaxLength="10000" HelperText="HTML 태그를 사용해 꾸밀 수 있습니다." />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="실시간 미리보기" Icon="@Icons.Material.Filled.Visibility">
|
||||
<div class="border rounded pa-4 article-body lh-lg" style="min-height: 330px; max-height: 500px; overflow-y: auto; background-color: #fafafa;">
|
||||
@if (string.IsNullOrWhiteSpace(model.Content))
|
||||
{
|
||||
<p class="text-muted small text-center my-8">작성 중인 본문 내용이 이곳에 실시간으로 표시됩니다.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@((MarkupString)model.Content)
|
||||
}
|
||||
</div>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
<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" />
|
||||
@@ -74,12 +61,24 @@
|
||||
private MudForm? form;
|
||||
private List<Domain.Entities.Category> categories = [];
|
||||
private CreatePostModel model = new();
|
||||
private EasyMDE.Editor? editor;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JS { get; set; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
categories = (await CategoryRepository.GetAllAsync()).ToList();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("window.initMarkdownEditor", "markdown-editor", model.Content ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
Navigation.NavigateTo("/taxbaik/admin/blog");
|
||||
@@ -90,6 +89,15 @@
|
||||
if (form == 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;
|
||||
@@ -127,3 +135,33 @@
|
||||
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>
|
||||
|
||||
@@ -44,24 +44,11 @@ else
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudTabs Elevation="2" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-4" Class="mb-4">
|
||||
<MudTabPanel Text="에디터" Icon="@Icons.Material.Filled.Edit">
|
||||
<MudTextField @bind-Value="model.Content" Label="본문 내용 *"
|
||||
Variant="Variant.Outlined" Lines="15" Required="true" RequiredError="본문 내용을 입력하세요." Counter="10000" MaxLength="10000" HelperText="HTML 태그를 사용해 꾸밀 수 있습니다." />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="실시간 미리보기" Icon="@Icons.Material.Filled.Visibility">
|
||||
<div class="border rounded pa-4 article-body lh-lg" style="min-height: 330px; max-height: 500px; overflow-y: auto; background-color: #fafafa;">
|
||||
@if (string.IsNullOrWhiteSpace(model.Content))
|
||||
{
|
||||
<p class="text-muted small text-center my-8">작성 중인 본문 내용이 이곳에 실시간으로 표시됩니다.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@((MarkupString)model.Content)
|
||||
}
|
||||
</div>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
<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" />
|
||||
@@ -88,6 +75,9 @@ else
|
||||
[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 = [];
|
||||
@@ -115,6 +105,14 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -136,6 +134,15 @@ else
|
||||
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;
|
||||
@@ -202,3 +209,33 @@ else
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user