feat(admin): stabilize blog and admin patterns
TaxBaik CI/CD / build-and-deploy (push) Has been cancelled
TaxBaik CI/CD / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -11,11 +11,6 @@
|
||||
<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',
|
||||
@@ -32,7 +27,7 @@
|
||||
<span style="font-size: 0.85rem; margin-top: 0.5rem; opacity: 0.8;">자동으로 페이지를 새로고침합니다. 잠시만 기다려주세요.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="blazor-loading" class="blazor-loading-overlay show">
|
||||
<div id="blazor-loading" class="blazor-loading-overlay">
|
||||
<div class="blazor-loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p>로드 중...</p>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using TaxBaik.Application.DTOs
|
||||
@using TaxBaik.Application.Services
|
||||
@using TaxBaik.Web.Components.Admin.Shared
|
||||
|
||||
<MudForm @ref="form">
|
||||
<MudTextField @bind-Value="model.Name" Label="이름"
|
||||
@@ -11,25 +12,12 @@
|
||||
<MudTextField @bind-Value="model.Email" Label="이메일"
|
||||
Variant="Variant.Outlined" Class="mb-4" InputType="InputType.Email" />
|
||||
|
||||
<MudSelect @bind-Value="model.ServiceType" Label="문의 유형"
|
||||
Variant="Variant.Outlined" Class="mb-4">
|
||||
<MudSelectItem Value="@("사업자세무")">사업자세무</MudSelectItem>
|
||||
<MudSelectItem Value="@("부동산세금")">부동산세금</MudSelectItem>
|
||||
<MudSelectItem Value="@("가족자산")">가족자산</MudSelectItem>
|
||||
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="model.ServiceType" Group="INQUIRY_SERVICE_TYPE" Label="문의 유형" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="model.Message" Label="문의 내용"
|
||||
Variant="Variant.Outlined" Lines="5" Class="mb-4" Required="true" />
|
||||
|
||||
<MudSelect @bind-Value="model.Status" Label="상태"
|
||||
Variant="Variant.Outlined" Class="mb-4">
|
||||
<MudSelectItem Value="@("new")">신규</MudSelectItem>
|
||||
<MudSelectItem Value="@("consulting")">상담중</MudSelectItem>
|
||||
<MudSelectItem Value="@("contracted")">계약완료</MudSelectItem>
|
||||
<MudSelectItem Value="@("rejected")">거절</MudSelectItem>
|
||||
<MudSelectItem Value="@("closed")">종결</MudSelectItem>
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="model.Status" Group="INQUIRY_STATUS" Label="상태" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="model.AdminMemo" Label="관리 메모"
|
||||
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
||||
@@ -57,8 +45,7 @@
|
||||
|
||||
private MudForm? form;
|
||||
private InquiryFormModel model = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (InitialData != null)
|
||||
{
|
||||
@@ -73,6 +60,7 @@
|
||||
AdminMemo = InitialData.AdminMemo
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
@page "/admin/blog/create"
|
||||
@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
|
||||
@using TaxBaik.Web.Components.Admin.Pages.Blog
|
||||
@inject IBlogBrowserClient BlogClient
|
||||
@inject ICategoryBrowserClient CategoryClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
@@ -21,62 +19,16 @@
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</MudForm>
|
||||
<BlogForm Model="model" Categories="categories" SubmitText="저장" OnSubmit="SavePost" OnCancel="GoBack" />
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private MudForm? form;
|
||||
private List<Domain.Entities.Category> categories = [];
|
||||
private CreatePostModel model = new();
|
||||
private EasyMDE.Editor? editor;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JS { get; set; } = null!;
|
||||
private IReadOnlyList<Domain.Entities.Category> categories = [];
|
||||
private BlogForm.BlogFormModel model = new();
|
||||
|
||||
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 ?? "");
|
||||
}
|
||||
categories = await CategoryClient.GetAllAsync();
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
@@ -86,25 +38,9 @@
|
||||
|
||||
private async Task SavePost()
|
||||
{
|
||||
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;
|
||||
|
||||
try
|
||||
{
|
||||
await BlogService.CreateAsync(new CreateBlogPostDto
|
||||
var result = await BlogClient.CreateAsync(new CreateBlogPostDto
|
||||
{
|
||||
Title = model.Title,
|
||||
Content = model.Content,
|
||||
@@ -115,6 +51,12 @@
|
||||
IsPublished = model.IsPublished
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Snackbar.Add("포스트 저장에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("포스트가 저장되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/blog");
|
||||
}
|
||||
@@ -123,45 +65,4 @@
|
||||
Snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private class CreatePostModel
|
||||
{
|
||||
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>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
@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
|
||||
@using TaxBaik.Web.Components.Admin.Pages.Blog
|
||||
@inject IBlogBrowserClient BlogClient
|
||||
@inject ICategoryBrowserClient CategoryClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@@ -32,42 +30,10 @@ else if (post == null)
|
||||
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>
|
||||
<BlogForm Model="model" Categories="categories" SubmitText="저장" OnSubmit="SavePost" />
|
||||
<div class="mt-4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Error" @onclick="DeletePost">삭제</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@@ -75,23 +41,19 @@ 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 = [];
|
||||
private EditPostModel model = new();
|
||||
private TaxBaik.Application.DTOs.BlogPostResponseDto? post;
|
||||
private IReadOnlyList<Domain.Entities.Category> categories = [];
|
||||
private BlogForm.BlogFormModel model = new();
|
||||
private bool isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
post = await BlogService.GetByIdAsync(Id);
|
||||
post = await BlogClient.GetByIdAsync(Id);
|
||||
if (post != null)
|
||||
{
|
||||
categories = (await CategoryRepository.GetAllAsync()).ToList();
|
||||
categories = await CategoryClient.GetAllAsync();
|
||||
MapPostToModel(post);
|
||||
}
|
||||
}
|
||||
@@ -105,15 +67,7 @@ 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)
|
||||
private void MapPostToModel(TaxBaik.Application.DTOs.BlogPostResponseDto post)
|
||||
{
|
||||
model.Title = post.Title;
|
||||
model.Content = post.Content;
|
||||
@@ -131,25 +85,12 @@ else
|
||||
|
||||
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)
|
||||
if (post == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await BlogService.UpdateAsync(post.Id, new CreateBlogPostDto
|
||||
var result = await BlogClient.UpdateAsync(post.Id, new CreateBlogPostDto
|
||||
{
|
||||
Title = model.Title,
|
||||
Content = model.Content,
|
||||
@@ -160,6 +101,12 @@ else
|
||||
IsPublished = model.IsPublished
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Snackbar.Add("저장 실패: 포스트를 저장하지 못했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("포스트가 저장되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/blog");
|
||||
}
|
||||
@@ -188,7 +135,12 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
await BlogService.DeleteAsync(post.Id);
|
||||
var deleted = await BlogClient.DeleteAsync(post.Id);
|
||||
if (!deleted)
|
||||
{
|
||||
Snackbar.Add("삭제 실패: 포스트를 삭제하지 못했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/blog");
|
||||
}
|
||||
@@ -197,45 +149,4 @@ else
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
@using TaxBaik.Application.DTOs
|
||||
@using TaxBaik.Domain.Entities
|
||||
|
||||
<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>
|
||||
|
||||
<MudTextField @bind-Value="Model.Content" Label="본문 내용 *"
|
||||
Variant="Variant.Outlined" Lines="16" Required="true" RequiredError="본문 내용을 입력하세요."
|
||||
Class="mb-4" />
|
||||
|
||||
<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="HandleSubmit">@SubmitText</MudButton>
|
||||
@if (OnCancel.HasDelegate)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" @onclick="OnCancel">취소</MudButton>
|
||||
}
|
||||
</div>
|
||||
</MudForm>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public BlogFormModel Model { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<Category> Categories { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public string SubmitText { get; set; } = "저장";
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnSubmit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
|
||||
private MudForm? form;
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
if (form == null)
|
||||
return;
|
||||
|
||||
await form.Validate();
|
||||
if (!form.IsValid)
|
||||
return;
|
||||
|
||||
await OnSubmit.InvokeAsync();
|
||||
}
|
||||
|
||||
public class BlogFormModel
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
@page "/admin/clients/{ClientId:int}"
|
||||
@attribute [Authorize]
|
||||
@using TaxBaik.Application.Services
|
||||
@inject ClientService ClientService
|
||||
@inject ConsultationService ConsultationService
|
||||
@using TaxBaik.Web.Services
|
||||
@using TaxBaik.Web.Services.AdminClients
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject IConsultingActivityBrowserClient ConsultingClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
@@ -102,8 +103,8 @@
|
||||
<MudDatePicker @bind-Date="newDate" Label="상담일" DateFormat="yyyy-MM-dd" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect T="string" @bind-Value="newServiceType" Label="서비스 분야">
|
||||
@foreach (var t in ClientService.ServiceTypes)
|
||||
<MudSelect T="string" @bind-Value="newServiceType" Label="서비스 분야">
|
||||
@foreach (var t in serviceTypes)
|
||||
{
|
||||
<MudSelectItem Value="@t">@t</MudSelectItem>
|
||||
}
|
||||
@@ -116,7 +117,7 @@
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect T="string" @bind-Value="newResult" Label="결과">
|
||||
<MudSelectItem Value="@("")">-</MudSelectItem>
|
||||
@foreach (var r in ConsultationService.Results)
|
||||
@foreach (var r in results)
|
||||
{
|
||||
<MudSelectItem Value="@r">@r</MudSelectItem>
|
||||
}
|
||||
@@ -182,6 +183,8 @@
|
||||
|
||||
private Domain.Entities.Client? client;
|
||||
private List<Domain.Entities.Consultation> consultations = [];
|
||||
private static readonly string[] serviceTypes = ["기장대리", "세무조정", "양도세", "증여세", "상속세", "부가세", "종소세", "기타"];
|
||||
private static readonly string[] results = ["", "상담완료", "추가자료 요청", "견적발송", "계약전환", "보류"];
|
||||
|
||||
private bool showAddForm;
|
||||
private DateTime? newDate = DateTime.Today;
|
||||
@@ -197,8 +200,19 @@
|
||||
|
||||
private async Task LoadAll()
|
||||
{
|
||||
client = await ClientService.GetByIdAsync(ClientId);
|
||||
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
|
||||
client = await ClientClient.GetByIdAsync(ClientId);
|
||||
consultations = (await ConsultingClient.GetByClientIdAsync(ClientId))
|
||||
.Select(c => new Domain.Entities.Consultation
|
||||
{
|
||||
Id = c.Id,
|
||||
ClientId = c.ClientId,
|
||||
ConsultationDate = c.ActivityDate,
|
||||
ServiceType = c.ActivityType,
|
||||
Summary = c.Description,
|
||||
Result = null,
|
||||
Fee = null
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void OpenAddConsultation()
|
||||
@@ -215,30 +229,35 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
var c = new Domain.Entities.Consultation
|
||||
{
|
||||
ClientId = ClientId,
|
||||
ConsultationDate = newDate?.ToUniversalTime() ?? DateTime.UtcNow,
|
||||
ServiceType = string.IsNullOrWhiteSpace(newServiceType) ? null : newServiceType,
|
||||
Summary = newSummary,
|
||||
Result = string.IsNullOrWhiteSpace(newResult) ? null : newResult,
|
||||
Fee = newFee
|
||||
};
|
||||
await ConsultationService.CreateAsync(c);
|
||||
var newId = await ConsultingClient.CreateAsync(
|
||||
ClientId,
|
||||
string.IsNullOrWhiteSpace(newServiceType) ? "기타" : newServiceType,
|
||||
newDate?.ToUniversalTime() ?? DateTime.UtcNow,
|
||||
newSummary,
|
||||
null,
|
||||
null);
|
||||
|
||||
if (newId <= 0)
|
||||
throw new Exception("상담 생성 실패");
|
||||
|
||||
showAddForm = false;
|
||||
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
|
||||
await LoadAll();
|
||||
Snackbar.Add("상담이 추가되었습니다.", Severity.Success);
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
Snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteConsultation(int id)
|
||||
{
|
||||
await ConsultationService.DeleteAsync(id);
|
||||
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
|
||||
await ConsultingClient.DeleteAsync(id);
|
||||
await LoadAll();
|
||||
Snackbar.Add("삭제되었습니다.", Severity.Info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using TaxBaik.Application.DTOs
|
||||
@using TaxBaik.Web.Services
|
||||
@using TaxBaik.Domain.Entities
|
||||
@using TaxBaik.Web.Components.Admin.Shared
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
@@ -54,20 +55,10 @@
|
||||
<MudDivider />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudSelect @bind-Value="dto.ServiceType" Label="서비스 유형" T="string" Clearable="true">
|
||||
@foreach (var t in ClientService.ServiceTypes)
|
||||
{
|
||||
<MudSelectItem Value="@t">@t</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="dto.ServiceType" Group="CLIENT_SERVICE_TYPE" Label="서비스 유형" Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudSelect @bind-Value="dto.TaxType" Label="세금 유형" T="string" Clearable="true">
|
||||
@foreach (var t in ClientService.TaxTypes)
|
||||
{
|
||||
<MudSelectItem Value="@t">@t</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="dto.TaxType" Group="CLIENT_TAX_TYPE" Label="세금 유형" Clearable="true" />
|
||||
</MudItem>
|
||||
|
||||
@* 관리 정보 *@
|
||||
@@ -76,18 +67,10 @@
|
||||
<MudDivider />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudSelect @bind-Value="dto.Status" Label="상태 *" T="string" Required="true">
|
||||
<MudSelectItem Value="@("active")">활성</MudSelectItem>
|
||||
<MudSelectItem Value="@("inactive")">비활성</MudSelectItem>
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="dto.Status" Group="CLIENT_STATUS" Label="상태 *" Required="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudSelect @bind-Value="dto.Source" Label="유입 경로" T="string" Clearable="true">
|
||||
@foreach (var s in ClientService.Sources)
|
||||
{
|
||||
<MudSelectItem Value="@s">@s</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="dto.Source" Group="CLIENT_SOURCE" Label="유입 경로" Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="dto.Memo" Label="메모"
|
||||
@@ -119,7 +102,6 @@
|
||||
private bool isValid;
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
|
||||
@@ -9,18 +9,15 @@
|
||||
|
||||
<PageTitle>고객 관리</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">고객 관리</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 카드를 등록하고 상담 이력을 관리합니다.</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.PersonAdd"
|
||||
Href="/taxbaik/admin/clients/create">
|
||||
고객 등록
|
||||
</MudButton>
|
||||
</section>
|
||||
<AdminPageHeader Title="고객 관리" Eyebrow="CRM" Subtitle="고객 카드를 등록하고 상담 이력을 관리합니다.">
|
||||
<ChildContent>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.PersonAdd"
|
||||
Href="/taxbaik/admin/clients/create">
|
||||
고객 등록
|
||||
</MudButton>
|
||||
</ChildContent>
|
||||
</AdminPageHeader>
|
||||
|
||||
@* 검색/필터 바 *@
|
||||
<MudPaper Class="admin-surface mb-3 pa-3" Elevation="0">
|
||||
@@ -53,10 +50,7 @@
|
||||
}
|
||||
else if (!clients.Any())
|
||||
{
|
||||
<div class="pa-6 text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.PeopleAlt" Style="font-size:3rem; opacity:.3;" />
|
||||
<MudText Class="mt-2 text-muted">등록된 고객이 없습니다.</MudText>
|
||||
</div>
|
||||
<AdminEmptyState Icon="@Icons.Material.Filled.PeopleAlt" Message="등록된 고객이 없습니다." />
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@page "/admin/inquiries/create"
|
||||
@attribute [Authorize]
|
||||
@using TaxBaik.Application.DTOs
|
||||
@using TaxBaik.Application.Services
|
||||
@using TaxBaik.Web.Components.Admin.Forms
|
||||
@inject InquiryService InquiryService
|
||||
@inject IInquiryBrowserClient InquiryClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
@@ -32,13 +31,21 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
await InquiryService.SubmitAsync(
|
||||
model.Name,
|
||||
model.Phone,
|
||||
model.ServiceType,
|
||||
model.Message,
|
||||
model.Email,
|
||||
ipAddress: "admin-registered");
|
||||
var result = await InquiryClient.CreateAsync(new SubmitInquiryDto
|
||||
{
|
||||
Name = model.Name,
|
||||
Phone = model.Phone,
|
||||
Email = model.Email,
|
||||
ServiceType = model.ServiceType,
|
||||
Message = model.Message,
|
||||
SuppressNotification = true
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Snackbar.Add("문의가 등록되지 않았습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("문의가 등록되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/inquiries");
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@page "/admin/inquiries/{id:int}/edit"
|
||||
@attribute [Authorize]
|
||||
@using TaxBaik.Application.DTOs
|
||||
@using TaxBaik.Application.Services
|
||||
@using TaxBaik.Web.Components.Admin.Forms
|
||||
@inject InquiryService InquiryService
|
||||
@inject IInquiryBrowserClient InquiryClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@@ -52,7 +51,7 @@ else
|
||||
{
|
||||
try
|
||||
{
|
||||
inquiry = await InquiryService.GetByIdAsync(Id);
|
||||
inquiry = await InquiryClient.GetByIdAsync(Id);
|
||||
if (inquiry != null)
|
||||
{
|
||||
formModel = new InquiryForm.InquiryFormModel
|
||||
@@ -89,19 +88,34 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
inquiry.Name = model.Name;
|
||||
inquiry.Phone = model.Phone;
|
||||
inquiry.Email = model.Email;
|
||||
inquiry.ServiceType = model.ServiceType;
|
||||
inquiry.Message = model.Message;
|
||||
inquiry.AdminMemo = model.AdminMemo;
|
||||
|
||||
if (inquiry.Status != model.Status)
|
||||
var updated = await InquiryClient.UpdateAsync(inquiry.Id, new UpdateInquiryDto
|
||||
{
|
||||
await InquiryService.UpdateStatusAsync(inquiry.Id, model.Status);
|
||||
Name = model.Name,
|
||||
Phone = model.Phone,
|
||||
Email = model.Email,
|
||||
ServiceType = model.ServiceType,
|
||||
Message = model.Message,
|
||||
Status = model.Status,
|
||||
AdminMemo = model.AdminMemo
|
||||
});
|
||||
|
||||
if (updated == null)
|
||||
{
|
||||
Snackbar.Add("문의 수정에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
await InquiryService.UpdateAdminMemoAsync(inquiry.Id, model.AdminMemo);
|
||||
inquiry = updated;
|
||||
formModel = new InquiryForm.InquiryFormModel
|
||||
{
|
||||
Name = inquiry.Name,
|
||||
Phone = inquiry.Phone,
|
||||
Email = inquiry.Email,
|
||||
ServiceType = inquiry.ServiceType,
|
||||
Message = inquiry.Message,
|
||||
Status = inquiry.Status,
|
||||
AdminMemo = inquiry.AdminMemo
|
||||
};
|
||||
|
||||
Snackbar.Add("문의가 수정되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/inquiries");
|
||||
@@ -131,7 +145,12 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
await InquiryService.DeleteAsync(inquiry.Id);
|
||||
var deleted = await InquiryClient.DeleteAsync(inquiry.Id);
|
||||
if (!deleted)
|
||||
{
|
||||
Snackbar.Add("문의 삭제에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
Snackbar.Add("문의가 삭제되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/inquiries");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/admin/revenue-trackings"
|
||||
@using TaxBaik.Web.Services.AdminClients
|
||||
@using TaxBaik.Web.Components.Admin.Shared
|
||||
@inject IRevenueTrackingBrowserClient RevenueClient
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject ISnackbar Snackbar
|
||||
@@ -102,13 +103,7 @@
|
||||
<MudTextField T="string" @bind-Value="revenueForm.InvoiceNumber" Label="청구번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||
<MudDatePicker @bind-Date="revenueForm.InvoiceDate" Label="청구일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||
<MudNumericField T="decimal" @bind-Value="revenueForm.Amount" Label="청구액" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||
<MudSelect T="string" @bind-Value="revenueForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||
<MudSelectItem Value="@("기장 수수료")">기장 수수료</MudSelectItem>
|
||||
<MudSelectItem Value="@("세무조정료")">세무조정료</MudSelectItem>
|
||||
<MudSelectItem Value="@("세무상담료")">세무상담료</MudSelectItem>
|
||||
<MudSelectItem Value="@("신고 대행료")">신고 대행료</MudSelectItem>
|
||||
<MudSelectItem Value="@("자문 수수료")">자문 수수료</MudSelectItem>
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="revenueForm.ServiceType" Group="REVENUE_SERVICE_TYPE" Label="서비스 유형" Class="mb-4" />
|
||||
<MudDatePicker @bind-Date="revenueForm.DueDate" Label="납부예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<MudTextField @bind-Value="email" Label="이메일"
|
||||
Variant="Variant.Outlined" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="kakaoUrl" Label="카카오 채널 URL"
|
||||
<MudTextField @bind-Value="kakaoUrl" Label="카카오채널 URL"
|
||||
Variant="Variant.Outlined" Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="instagramUrl" Label="인스타그램"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using TaxBaik.Web.Services
|
||||
@using TaxBaik.Domain.Entities
|
||||
@using TaxBaik.Web.Components.Admin.Shared
|
||||
@inject ITaxFilingBrowserClient FilingClient
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
@@ -21,10 +22,10 @@ else
|
||||
<RowTemplate>
|
||||
<MudTd>@context.ClientName</MudTd>
|
||||
<MudTd>@context.FilingType</MudTd>
|
||||
<MudTd>@context.DueDate.ToString("yyyy-MM-dd")</MudTd>
|
||||
<MudTd>@BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(context.DueDate)).ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")</MudTd>
|
||||
<MudTd>
|
||||
@{
|
||||
var dday = (context.DueDate.Date - DateTime.Today).Days;
|
||||
var dday = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(context.DueDate));
|
||||
}
|
||||
@if (dday < 0)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@attribute [Authorize]
|
||||
@using TaxBaik.Web.Services
|
||||
@using TaxBaik.Domain.Entities
|
||||
@using TaxBaik.Web.Components.Admin.Shared
|
||||
@inject ITaxFilingBrowserClient FilingClient
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject ISnackbar Snackbar
|
||||
@@ -34,12 +35,7 @@
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="string" @bind-Value="newFilingType" Label="신고 유형 *" Variant="Variant.Outlined">
|
||||
@foreach (var t in TaxFilingService.FilingTypes)
|
||||
{
|
||||
<MudSelectItem Value="@t">@t</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<CommonCodeSelect @bind-Value="newFilingType" Group="FILING_TYPE" Label="신고 유형 *" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudDatePicker @bind-Date="newDueDate" Label="신고 기한 *" DateFormat="yyyy-MM-dd" />
|
||||
@@ -82,6 +78,10 @@
|
||||
|
||||
protected override async Task OnInitializedAsync() => await Reload();
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task Reload()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="pa-6 text-center">
|
||||
<MudIcon Icon="@Icon" Style="font-size:3rem; opacity:.3;" />
|
||||
<MudText Class="mt-2 text-muted">@Message</MudText>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.Info;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string Message { get; set; } = "";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
@if (!string.IsNullOrWhiteSpace(Eyebrow))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">@Eyebrow</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">@Title</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(Subtitle))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">@Subtitle</MudText>
|
||||
}
|
||||
</div>
|
||||
@if (ChildContent is not null)
|
||||
{
|
||||
<div>@ChildContent</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public string? Eyebrow { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Subtitle { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -32,6 +32,16 @@ public class BlogController : ControllerBase
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
[HttpGet("admin/{id:int}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
var post = await _blogService.GetByIdAsync(id);
|
||||
if (post == null)
|
||||
return NotFound(new ProblemDetails { Title = "포스트를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound });
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
[HttpGet("admin/all")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetAll()
|
||||
@@ -84,7 +94,7 @@ public class BlogController : ControllerBase
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _blogService.DeleteAsync(id);
|
||||
await _blogService.ArchiveAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Application.Services;
|
||||
|
||||
namespace TaxBaik.Web.Controllers;
|
||||
@@ -19,7 +20,7 @@ public class InquiryController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Submit([FromBody] SubmitInquiryRequest request)
|
||||
public async Task<IActionResult> Submit([FromBody] SubmitInquiryDto request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Phone))
|
||||
return BadRequest(new ProblemDetails { Title = "이름과 전화번호를 입력하세요.", Status = StatusCodes.Status400BadRequest });
|
||||
@@ -99,6 +100,23 @@ public class InquiryController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] UpdateInquiryDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _inquiryService.UpdateAsync(id, request);
|
||||
if (result == null)
|
||||
return NotFound(new ProblemDetails { Title = "문의를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound });
|
||||
return Ok(result);
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/convert-to-client")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ConvertToClient(int id, [FromBody] ConvertToClientRequest request)
|
||||
@@ -129,16 +147,6 @@ public class InquiryController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
public class SubmitInquiryRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string? Email { get; set; }
|
||||
public string ServiceType { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public bool SuppressNotification { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateStatusRequest
|
||||
{
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
<p class="text-muted mb-5">무료 상담으로 현재 상황을 진단하고 맞춤형 절세 전략을 받아보세요.</p>
|
||||
<div class="d-flex gap-3 justify-content-center flex-wrap">
|
||||
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">상담 신청하기</a>
|
||||
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn btn-warning btn-lg">카카오로 문의</a>
|
||||
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn btn-warning btn-lg">카카오채널로 문의</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using Markdig;
|
||||
using System.Net;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Blog;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class BlogPostModel : PageModel
|
||||
Post = await _blogService.GetBySlugAsync(slug);
|
||||
if (Post != null)
|
||||
{
|
||||
HtmlContent = Markdown.ToHtml(Post.Content ?? "");
|
||||
HtmlContent = WebUtility.HtmlEncode(Post.Content ?? "").Replace("\r\n", "<br />").Replace("\n", "<br />");
|
||||
_ = _blogService.IncrementViewCountAsync(Post.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<option value="기장">사업자 기장</option>
|
||||
<option value="양도세">부동산 양도세</option>
|
||||
<option value="종소세">종합소득세</option>
|
||||
<option value="증여상속">증여·상속세</option>
|
||||
<option value="증여상속">증여상속세</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
var season = Model.CurrentSeason;
|
||||
ViewData["Title"] = season != null
|
||||
? $"백원숙 세무회계 | {season.Name} — 지금 상담하세요"
|
||||
: "백원숙 세무회계 | 사업자·부동산·증여 세무 상담";
|
||||
ViewData["Description"] = "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 온라인 맞춤 상담 제공.";
|
||||
: "백원숙 세무회계 | 사업자·부동산·증여상속 세무 상담";
|
||||
ViewData["Description"] = "사업자 기장, 부동산 양도세·증여상속, 종합소득세 전문 상담. 온라인 맞춤 상담 제공.";
|
||||
}
|
||||
|
||||
@* ─── 공지사항 배너 (관리자 등록 공지) ─── *@
|
||||
@@ -50,7 +50,7 @@
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="btn btn-outline-primary btn-lg"
|
||||
onclick="openKakao()" style="border-color: white; color: white;">
|
||||
💬 카카오 채널 문의
|
||||
💬 카카오채널 문의
|
||||
</a>
|
||||
</div>
|
||||
@if (season.DaysUntilDeadline <= 7)
|
||||
@@ -91,7 +91,7 @@ else
|
||||
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">무료 상담 신청</a>
|
||||
<a href="javascript:void(0);" class="btn btn-outline-primary btn-lg"
|
||||
onclick="openKakao()" style="border-color: white; color: white;">
|
||||
💬 카카오 채널 문의
|
||||
💬 카카오채널 문의
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ else
|
||||
<div class="service-icon">👨👩👧👦</div>
|
||||
<div class="card-body pt-0">
|
||||
<h3 class="card-title">가족자산 관리</h3>
|
||||
<p class="text-muted small">증여·상속 사전 계획부터 대표자 리스크 관리까지 — 가족 자산을 지키는 전략.</p>
|
||||
<p class="text-muted small">증여상속 사전 계획부터 대표자 리스크 관리까지 - 가족 자산을 지키는 전략.</p>
|
||||
<a href="/taxbaik/services#family-asset" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,7 +362,7 @@ else
|
||||
</p>
|
||||
<div class="d-flex gap-3 justify-content-center flex-wrap">
|
||||
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">⏰ @season.CtaText</a>
|
||||
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오로 문의</a>
|
||||
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오채널로 문의</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -374,7 +374,7 @@ else
|
||||
</p>
|
||||
<div class="d-flex gap-3 justify-content-center flex-wrap">
|
||||
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">상담 신청하기</a>
|
||||
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오로 문의</a>
|
||||
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오채널로 문의</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -21,51 +21,9 @@ window.taxbaikAdminSession = {
|
||||
},
|
||||
|
||||
showLoading: function () {
|
||||
if (document.documentElement.classList.contains('admin-login-route')) {
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = document.getElementById('blazor-loading');
|
||||
if (!overlay) return;
|
||||
|
||||
// Show overlay immediately
|
||||
overlay.classList.add('show');
|
||||
|
||||
// Check if page is already ready (cached state on fast nav)
|
||||
const pageReady =
|
||||
document.querySelector('.admin-page-hero') !== null ||
|
||||
document.querySelector('.admin-login-page') !== null;
|
||||
if (pageReady) {
|
||||
// Page already rendered, hide immediately
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start observer to catch future mutations
|
||||
if (window._taxbaikLoadingObserver) {
|
||||
window._taxbaikLoadingObserver.disconnect();
|
||||
}
|
||||
window._taxbaikLoadingObserver = new MutationObserver(function () {
|
||||
const pageReady =
|
||||
document.querySelector('.admin-page-hero') !== null ||
|
||||
document.querySelector('.admin-login-page') !== null;
|
||||
if (pageReady) {
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
}
|
||||
});
|
||||
window._taxbaikLoadingObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Safety fallback: hide after 3 seconds regardless.
|
||||
if (window._taxbaikLoadingTimeout) {
|
||||
clearTimeout(window._taxbaikLoadingTimeout);
|
||||
}
|
||||
window._taxbaikLoadingTimeout = setTimeout(function () {
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
}, 3000);
|
||||
// Route transitions are handled by Blazor; avoid full-screen overlays
|
||||
// that block drawer interaction and make the app feel frozen.
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
},
|
||||
|
||||
hideLoading: function () {
|
||||
@@ -93,9 +51,8 @@ window.taxbaikAdminSession = {
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
}
|
||||
|
||||
// Show loading on initial page load — overlay has 'show' from HTML,
|
||||
// but we still need to set up the observer to detect when to hide it.
|
||||
window.taxbaikAdminSession.showLoading();
|
||||
// Keep the initial overlay hidden unless explicitly enabled elsewhere.
|
||||
window.taxbaikAdminSession.hideLoading();
|
||||
|
||||
const modal = document.getElementById('components-reconnect-modal');
|
||||
if (!modal) return;
|
||||
|
||||
@@ -65,9 +65,9 @@
|
||||
보통 <strong>1~2분</strong> 이내에 완료됩니다.
|
||||
</p>
|
||||
<hr class="divider" />
|
||||
<p>급하신 세무 문의는 카카오 채널로 연락해 주세요.</p>
|
||||
<p>급하신 세무 문의는 카카오채널로 연락해 주세요.</p>
|
||||
<a class="kakao-btn" href="http://pf.kakao.com/_xoxchTX" target="_blank">
|
||||
💬 카카오 채널 상담
|
||||
💬 카카오채널 상담
|
||||
</a>
|
||||
<p class="timer">이 페이지는 15초 후 자동으로 새로고침됩니다.</p>
|
||||
<p class="footer">© 2026 백원숙 세무회계</p>
|
||||
|
||||
Reference in New Issue
Block a user