feat(admin): 공지사항, FAQ, 블로그 목록 검색 필터 추가 및 블로그 미리보기 탭 탑재, FAQ 순서 조정 기능 구현
This commit is contained in:
@@ -22,16 +22,21 @@
|
||||
</MudButton>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
@if (faqs is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else if (!faqs.Any())
|
||||
else if (!FilteredFaqs.Any())
|
||||
{
|
||||
<div class="pa-6 text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.QuestionAnswer" Style="font-size:3rem; opacity:.3;" />
|
||||
<MudText Class="mt-2 text-muted">등록된 FAQ가 없습니다.</MudText>
|
||||
<MudText Class="mt-2 text-muted">검색 조건에 맞는 FAQ가 없습니다.</MudText>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -39,7 +44,7 @@
|
||||
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:60px;">순서</th>
|
||||
<th style="width:110px;">순서</th>
|
||||
<th>질문</th>
|
||||
<th style="width:130px;">카테고리</th>
|
||||
<th style="width:90px;">상태</th>
|
||||
@@ -47,11 +52,15 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in faqs)
|
||||
@foreach (var item in FilteredFaqs)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<MudText Typo="Typo.body2">@item.SortOrder</MudText>
|
||||
<td>
|
||||
<div class="d-flex align-center justify-start gap-1">
|
||||
<MudText Typo="Typo.body2" Class="mr-2">@item.SortOrder</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowDropUp" Size="Size.Small" OnClick="@(() => MoveUpAsync(item))" Style="padding:2px;" Dense="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowDropDown" Size="Size.Small" OnClick="@(() => MoveDownAsync(item))" Style="padding:2px;" Dense="true" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2" Style="max-width:480px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
||||
@@ -77,10 +86,10 @@
|
||||
<td>
|
||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
||||
<MudButton @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/faqs/{item.Id}/edit"))">
|
||||
수정
|
||||
수정
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Error" @onclick="@(() => DeleteAsync(item))">
|
||||
삭제
|
||||
삭제
|
||||
</MudButton>
|
||||
</MudButtonGroup>
|
||||
</td>
|
||||
@@ -89,7 +98,7 @@
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
<MudText Typo="Typo.caption" Class="pa-2 text-muted">
|
||||
총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
|
||||
검색 결과 @(FilteredFaqs.Count())개 · 총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -99,6 +108,13 @@
|
||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||
|
||||
private List<Faq>? faqs;
|
||||
private string searchQuery = "";
|
||||
|
||||
private IEnumerable<Faq> FilteredFaqs => faqs?
|
||||
.Where(f => string.IsNullOrEmpty(searchQuery) ||
|
||||
f.Question.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
(f.Answer != null && f.Answer.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(f => f.SortOrder) ?? Enumerable.Empty<Faq>();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -120,7 +136,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
faqs = (await FaqClient.GetAllAsync()).ToList();
|
||||
faqs = (await FaqClient.GetAllAsync()).OrderBy(f => f.SortOrder).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -129,6 +145,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoveUpAsync(Faq item)
|
||||
{
|
||||
if (faqs == null) return;
|
||||
var sorted = faqs.OrderBy(f => f.SortOrder).ToList();
|
||||
var index = sorted.IndexOf(item);
|
||||
if (index <= 0) return;
|
||||
|
||||
var prev = sorted[index - 1];
|
||||
var temp = item.SortOrder;
|
||||
item.SortOrder = prev.SortOrder;
|
||||
prev.SortOrder = temp;
|
||||
|
||||
if (item.SortOrder == prev.SortOrder)
|
||||
{
|
||||
prev.SortOrder = item.SortOrder + 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await FaqClient.UpdateAsync(item.Id, item);
|
||||
await FaqClient.UpdateAsync(prev.Id, prev);
|
||||
Snackbar.Add("순서가 상향되었습니다.", Severity.Success);
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"순서 조정 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoveDownAsync(Faq item)
|
||||
{
|
||||
if (faqs == null) return;
|
||||
var sorted = faqs.OrderBy(f => f.SortOrder).ToList();
|
||||
var index = sorted.IndexOf(item);
|
||||
if (index < 0 || index >= sorted.Count - 1) return;
|
||||
|
||||
var next = sorted[index + 1];
|
||||
var temp = item.SortOrder;
|
||||
item.SortOrder = next.SortOrder;
|
||||
next.SortOrder = temp;
|
||||
|
||||
if (item.SortOrder == next.SortOrder)
|
||||
{
|
||||
next.SortOrder = item.SortOrder + 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await FaqClient.UpdateAsync(item.Id, item);
|
||||
await FaqClient.UpdateAsync(next.Id, next);
|
||||
Snackbar.Add("순서가 하향되었습니다.", Severity.Success);
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"순서 조정 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(Faq item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
|
||||
Reference in New Issue
Block a user