개선: 배포 검증과 관리자 UX 안정화
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var (items, _) = await InquiryService.GetPagedAsync(1, 1000);
|
||||
var (items, _) = await InquiryService.GetPagedAsync(1, 100);
|
||||
inquiries = items.ToList();
|
||||
FilterInquiries();
|
||||
}
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<MudThemeProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="ml-3">백원숙 세무회계 관리자</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Color="Color.Inherit" Href="/taxbaik">공개 사이트</MudButton>
|
||||
<MudButton Href="/taxbaik/admin/logout">로그아웃</MudButton>
|
||||
</MudAppBar>
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="ml-3">백원숙 세무회계 관리자</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Color="Color.Inherit" Href="/taxbaik">공개 사이트</MudButton>
|
||||
<MudButton Href="/taxbaik/admin/logout">로그아웃</MudButton>
|
||||
</MudAppBar>
|
||||
<MudDrawer @bind-open="@drawerOpen" Elevation="1" Variant="DrawerVariant.Responsive" Breakpoint="Breakpoint.Md">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All">📊 대시보드</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/blog">📝 블로그 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/inquiries">💬 문의 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/settings">⚙️ 설정</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
|
||||
<MudDrawer @bind-open="@drawerOpen" Elevation="1">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All">📊 대시보드</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/blog">📝 블로그 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/inquiries">💬 문의 관리</MudNavLink>
|
||||
<MudNavLink Href="/taxbaik/admin/settings">⚙️ 설정</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="my-4">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
@Body
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="my-4 px-3 px-md-0">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
private bool drawerOpen = true;
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
Href="/taxbaik/admin/blog/create">새 포스트</MudButton>
|
||||
</div>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1">@($"전체 포스트 {totalPosts}개")</MudText>
|
||||
<MudText Typo="Typo.body2">페이지 @currentPage / @totalPages</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudDataGrid Items="@posts" Striped="true" Hoverable="true" Loading="@isLoading">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Title" Title="제목" />
|
||||
@@ -32,9 +39,18 @@
|
||||
</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>
|
||||
|
||||
@code {
|
||||
private List<TaxBaik.Domain.Entities.BlogPost> posts = [];
|
||||
private bool isLoading = true;
|
||||
private int currentPage = 1;
|
||||
private int totalPages = 1;
|
||||
private int totalPosts = 0;
|
||||
private const int PageSize = 20;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -46,13 +62,38 @@
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
var items = await ApiClient.GetAsync<List<TaxBaik.Domain.Entities.BlogPost>>("blog/admin/all");
|
||||
posts = items ?? [];
|
||||
var result = await ApiClient.GetAsync<PagedBlogResponse>($"blog/admin?page={currentPage}&pageSize={PageSize}");
|
||||
posts = result?.Data ?? [];
|
||||
totalPosts = result?.Total ?? 0;
|
||||
totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize));
|
||||
}
|
||||
catch
|
||||
{
|
||||
posts = [];
|
||||
totalPosts = 0;
|
||||
totalPages = 1;
|
||||
}
|
||||
catch { }
|
||||
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.Domain.Entities.BlogPost post, bool isPublished)
|
||||
{
|
||||
var previous = post.IsPublished;
|
||||
@@ -86,4 +127,10 @@
|
||||
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
||||
await LoadPosts();
|
||||
}
|
||||
|
||||
private class PagedBlogResponse
|
||||
{
|
||||
public List<TaxBaik.Domain.Entities.BlogPost> Data { get; set; } = [];
|
||||
public int Total { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@page "/admin/dashboard"
|
||||
@using TaxBaik.Application.Services
|
||||
@inject InquiryService InquiryService
|
||||
@inject BlogService BlogService
|
||||
@inject AdminDashboardService DashboardService
|
||||
|
||||
<PageTitle>대시보드</PageTitle>
|
||||
|
||||
@@ -11,28 +10,28 @@
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2">이번달 문의</MudText>
|
||||
<MudText Typo="Typo.h4">@thisMonthInquiries</MudText>
|
||||
<MudText Typo="Typo.h4">@summary.ThisMonthInquiries</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2">신규 문의</MudText>
|
||||
<MudText Typo="Typo.h4">@newInquiries</MudText>
|
||||
<MudText Typo="Typo.h4">@summary.NewInquiries</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2">전체 포스트</MudText>
|
||||
<MudText Typo="Typo.h4">@totalPosts</MudText>
|
||||
<MudText Typo="Typo.h4">@summary.TotalPosts</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2">발행된 포스트</MudText>
|
||||
<MudText Typo="Typo.h4">@publishedPosts</MudText>
|
||||
<MudText Typo="Typo.h4">@summary.PublishedPosts</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -50,7 +49,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var inquiry in recentInquiries)
|
||||
@foreach (var inquiry in summary.RecentInquiries)
|
||||
{
|
||||
<tr>
|
||||
<td>@inquiry.Name</td>
|
||||
@@ -70,22 +69,10 @@
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private int thisMonthInquiries = 0;
|
||||
private int newInquiries = 0;
|
||||
private int totalPosts = 0;
|
||||
private int publishedPosts = 0;
|
||||
private List<Domain.Entities.Inquiry> recentInquiries = [];
|
||||
private AdminDashboardSummary summary = new(0, 0, 0, 0, []);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var (inquiries, _) = await InquiryService.GetPagedAsync(1, 100);
|
||||
recentInquiries = inquiries.OrderByDescending(x => x.CreatedAt).Take(5).ToList();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
thisMonthInquiries = inquiries.Count(x => x.CreatedAt.Year == now.Year && x.CreatedAt.Month == now.Month);
|
||||
newInquiries = inquiries.Count(x => x.Status == "new");
|
||||
var stats = await BlogService.GetStatsAsync();
|
||||
totalPosts = stats.TotalPosts;
|
||||
publishedPosts = stats.PublishedPosts;
|
||||
summary = await DashboardService.GetSummaryAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await ApiClient.SetAuthToken(response.Token);
|
||||
await AuthStateProvider.LoginAsync(response.Token);
|
||||
NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: false);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/admin/settings"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Collections.Generic
|
||||
@using TaxBaik.Web.Services
|
||||
@using TaxBaik.Domain.Interfaces
|
||||
@inject IApiClient ApiClient
|
||||
@@ -58,12 +59,65 @@
|
||||
private string newPassword = "";
|
||||
private string confirmNewPassword = "";
|
||||
private bool isChangingPassword;
|
||||
private bool isLoadingSettings;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadSettingsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadSettingsAsync()
|
||||
{
|
||||
isLoadingSettings = true;
|
||||
|
||||
try
|
||||
{
|
||||
var settings = await ApiClient.GetAsync<Dictionary<string, string>>("site-settings");
|
||||
if (settings is null || settings.Count == 0)
|
||||
return;
|
||||
|
||||
if (settings.TryGetValue("PhoneNumber", out var loadedPhone) && !string.IsNullOrWhiteSpace(loadedPhone))
|
||||
phone = loadedPhone;
|
||||
|
||||
if (settings.TryGetValue("EmailAddress", out var loadedEmail) && !string.IsNullOrWhiteSpace(loadedEmail))
|
||||
email = loadedEmail;
|
||||
|
||||
if (settings.TryGetValue("KakaoChannelUrl", out var loadedKakao) && !string.IsNullOrWhiteSpace(loadedKakao))
|
||||
kakaoUrl = loadedKakao;
|
||||
|
||||
if (settings.TryGetValue("InstagramUrl", out var loadedInstagram) && !string.IsNullOrWhiteSpace(loadedInstagram))
|
||||
instagramUrl = loadedInstagram;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Snackbar.Add("사이트 설정을 불러오지 못했습니다.", Severity.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
// TODO: Save settings to database
|
||||
Snackbar.Add("설정 저장 기능은 아직 구현되지 않았습니다.", Severity.Info);
|
||||
await Task.CompletedTask;
|
||||
if (isLoadingSettings)
|
||||
return;
|
||||
|
||||
var response = await ApiClient.PutAsync<SaveSettingsResponse>("site-settings", new
|
||||
{
|
||||
Phone = phone,
|
||||
Email = email,
|
||||
KakaoUrl = kakaoUrl,
|
||||
InstagramUrl = instagramUrl
|
||||
});
|
||||
|
||||
if (response?.Message is null)
|
||||
{
|
||||
Snackbar.Add("설정 저장에 실패했습니다.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add(response.Message, Severity.Success);
|
||||
}
|
||||
|
||||
private async Task ChangePassword()
|
||||
@@ -118,4 +172,9 @@
|
||||
{
|
||||
public string Message { get; set; } = "";
|
||||
}
|
||||
|
||||
private class SaveSettingsResponse
|
||||
{
|
||||
public string Message { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>찾을 수 없음</PageTitle>
|
||||
<LayoutView Layout="typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)">
|
||||
<p>요청한 페이지를 찾을 수 없습니다.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>찾을 수 없음</PageTitle>
|
||||
<LayoutView Layout="@typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)">
|
||||
<p>요청한 페이지를 찾을 수 없습니다.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
||||
@@ -32,6 +32,10 @@ public class BlogController : ControllerBase
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
[HttpGet("~/blog/{slug}")]
|
||||
public IActionResult RedirectToBlogPage(string slug)
|
||||
=> RedirectPermanent($"/taxbaik/blog/{slug}");
|
||||
|
||||
[HttpGet("admin/all")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetAll()
|
||||
@@ -40,6 +44,14 @@ public class BlogController : ControllerBase
|
||||
return Ok(posts);
|
||||
}
|
||||
|
||||
[HttpGet("admin")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetAdminPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
var (items, total) = await _blogService.GetAdminPagedAsync(page, pageSize);
|
||||
return Ok(new { data = items, total, page, pageSize });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Create([FromBody] CreateBlogPostDto dto)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TaxBaik.Application.Services;
|
||||
|
||||
namespace TaxBaik.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class SiteSettingsController : ControllerBase
|
||||
{
|
||||
private readonly SiteSettingService _siteSettingService;
|
||||
|
||||
public SiteSettingsController(SiteSettingService siteSettingService)
|
||||
{
|
||||
_siteSettingService = siteSettingService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get()
|
||||
{
|
||||
var settings = await _siteSettingService.GetAllAsync();
|
||||
return Ok(settings);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> Save([FromBody] SaveSiteSettingsRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
return BadRequest(new { message = "요청 본문이 비어 있습니다." });
|
||||
|
||||
await _siteSettingService.SaveAsync(request.Phone, request.Email, request.KakaoUrl, request.InstagramUrl);
|
||||
return Ok(new { message = "사이트 설정이 저장되었습니다." });
|
||||
}
|
||||
}
|
||||
|
||||
public class SaveSiteSettingsRequest
|
||||
{
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string KakaoUrl { get; set; } = string.Empty;
|
||||
public string InstagramUrl { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1 class="fw-bold mb-5">세무 블로그</h1>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-4 d-flex flex-wrap gap-2">
|
||||
<a href="/taxbaik/blog" class="btn btn-sm @(Model.SelectedCategoryId == null ? "btn-primary" : "btn-outline-primary")">전체</a>
|
||||
@foreach (var cat in Model.Categories)
|
||||
{
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="row g-4">
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<small class="badge bg-primary">@post.CategoryName</small>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@page "{slug}"
|
||||
@page "/blog/{slug}"
|
||||
@model TaxBaik.Web.Pages.Blog.BlogPostModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Post?.SeoTitle ?? Model.Post?.Title;
|
||||
ViewData["Description"] = Model.Post?.SeoDescription ?? "";
|
||||
ViewData["OgImage"] = Model.Post?.ThumbnailUrl ?? "";
|
||||
ViewData["CanonicalUrl"] = $"http://178.104.200.7/taxbaik/blog/{Model.Post?.Slug}";
|
||||
var canonicalUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}/blog/{Model.Post?.Slug}";
|
||||
ViewData["CanonicalUrl"] = canonicalUrl;
|
||||
ViewData["OgUrl"] = canonicalUrl;
|
||||
}
|
||||
|
||||
@if (Model.Post != null)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<div id="contact-success" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["Success"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<header class="sticky-top bg-white border-bottom">
|
||||
<nav class="navbar navbar-expand-lg navbar-light container-fluid px-3">
|
||||
<header class="sticky-top bg-white border-bottom site-header">
|
||||
<nav class="navbar navbar-expand-lg navbar-light container-fluid px-3 py-2">
|
||||
<a class="navbar-brand fw-bold" href="/taxbaik">
|
||||
<span class="text-primary">백원숙</span> 세무회계
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto gap-2">
|
||||
<div class="collapse navbar-collapse mt-3 mt-lg-0" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto gap-2 align-items-lg-center">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/taxbaik">홈</a>
|
||||
</li>
|
||||
@@ -21,7 +21,7 @@
|
||||
<a class="nav-link" href="/taxbaik/blog">블로그</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-primary btn-sm ms-2" href="/taxbaik/contact">상담신청</a>
|
||||
<a class="btn btn-primary btn-sm ms-lg-2 w-100 w-lg-auto" href="/taxbaik/contact">상담신청</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +142,60 @@ if (!app.Environment.IsDevelopment())
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.MapRazorPages();
|
||||
app.MapGet("/blog/{slug}", async (string slug, TaxBaik.Application.Services.BlogService blogService, HttpContext context) =>
|
||||
{
|
||||
var post = await blogService.GetBySlugAsync(slug, context.RequestAborted);
|
||||
if (post == null)
|
||||
return Results.NotFound();
|
||||
|
||||
var baseUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}";
|
||||
var canonical = $"{baseUrl}/blog/{post.Slug}";
|
||||
var html = $"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{System.Net.WebUtility.HtmlEncode(post.SeoTitle ?? post.Title)}</title>
|
||||
<meta name="description" content="{System.Net.WebUtility.HtmlEncode(post.SeoDescription ?? string.Empty)}" />
|
||||
<link rel="canonical" href="{canonical}" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>{System.Net.WebUtility.HtmlEncode(post.Title)}</h1>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
return Results.Content(html, "text/html; charset=utf-8");
|
||||
});
|
||||
app.MapGet("/taxbaik/blog/{slug}", async (string slug, TaxBaik.Application.Services.BlogService blogService, HttpContext context) =>
|
||||
{
|
||||
var post = await blogService.GetBySlugAsync(slug, context.RequestAborted);
|
||||
if (post == null)
|
||||
return Results.NotFound();
|
||||
|
||||
var baseUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}";
|
||||
var canonical = $"{baseUrl}/blog/{post.Slug}";
|
||||
var html = $"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{System.Net.WebUtility.HtmlEncode(post.SeoTitle ?? post.Title)}</title>
|
||||
<meta name="description" content="{System.Net.WebUtility.HtmlEncode(post.SeoDescription ?? string.Empty)}" />
|
||||
<link rel="canonical" href="{canonical}" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>{System.Net.WebUtility.HtmlEncode(post.Title)}</h1>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
return Results.Content(html, "text/html; charset=utf-8");
|
||||
});
|
||||
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>().AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -58,7 +58,13 @@ public class TelegramInquiryNotificationService : IInquiryNotificationService
|
||||
{
|
||||
var response = await client.PostAsJsonAsync(url, payload, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("텔레그램 알림 전송 실패: {StatusCode}", response.StatusCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("텔레그램 새 문의 알림 전송 성공: #{InquiryId}", inquiryId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -100,7 +106,13 @@ public class TelegramInquiryNotificationService : IInquiryNotificationService
|
||||
{
|
||||
var response = await client.PostAsJsonAsync(url, payload, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("텔레그램 상태 변경 알림 실패: {StatusCode}", response.StatusCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("텔레그램 상태 변경 알림 전송 성공: #{InquiryId}", inquiryId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -426,6 +426,38 @@ body.with-mobile-cta {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.site-header .navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.site-header .navbar-nav {
|
||||
padding: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.site-header .nav-link,
|
||||
.site-header .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.site-header .navbar-toggler {
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.site-header .navbar-collapse {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
min-width: 2.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
@@ -445,6 +477,10 @@ body.with-mobile-cta {
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.hero-section .d-flex {
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 일반 유틸리티 ===== */
|
||||
|
||||
@@ -8,13 +8,17 @@ function openKakao() {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Sticky header shadow
|
||||
const navbar = document.querySelector('.navbar');
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.scrollY > 0) {
|
||||
navbar.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
} else {
|
||||
navbar.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||
}
|
||||
});
|
||||
if (!navbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setShadow = () => {
|
||||
navbar.style.boxShadow = window.scrollY > 0
|
||||
? '0 2px 8px rgba(0,0,0,0.1)'
|
||||
: '0 1px 3px rgba(0,0,0,0.1)';
|
||||
};
|
||||
|
||||
setShadow();
|
||||
window.addEventListener('scroll', setShadow, { passive: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user