구현: W3 공개 홈페이지 (Razor Pages SSR)
공개 사이트 (port 5001):
- Program.cs: AddRazorPages, AddInfrastructure, AddApplication, UsePathBase("/taxbaik")
- appsettings.json: PostgreSQL 연결 문자열
레이아웃 및 공통 컴포넌트:
- _Layout.cshtml: Bootstrap 5, Noto Sans KR, OG 메타 태그, 모바일 고정 CTA 바
- _Header.cshtml: sticky navbar, 로고, 네비게이션 링크, 상담신청 버튼
- _Footer.cshtml: 사업자정보, 연락처, KakaoTalk, 저작권
- _ViewImports.cshtml, _ViewStart.cshtml
- site.css: CSS 변수 (--color-primary, --color-cta 등), 반응형 스타일
- site.js: 모바일 CTA 바 제어, sticky 헤더 효과
페이지 구현:
1. Index.cshtml (메인 랜딩): Hero, 신뢰도 strip, 서비스 카드, 최근 블로그
2. Services.cshtml (서비스): 4개 서비스 소개, 상담료 안내
3. About.cshtml (소개): 세무사 프로필, 3개 자격증, 서비스 철학
4. Contact.cshtml (상담신청): 폼 (이름, 전화, 이메일, 분야, 문의), ValidationException 처리
5. Blog/Index.cshtml (블로그 목록): 카테고리 필터 탭, 12개 그리드, 페이지네이션
6. Blog/Post.cshtml (포스트 상세): 브레드크럼, 제목, 메타정보, 콘텐츠, CTA, 공유 버튼
SEO:
- robots.txt: /taxbaik 허용, /admin 차단, sitemap 링크
- Sitemap.cshtml: 동적 생성 (정적 페이지 + 모든 포스트)
기술:
- Dapper 기반 DB 접근
- 페이징: 12개/페이지
- 한국어 입력값 검증
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
@page
|
||||
@model BlogIndexModel
|
||||
@{
|
||||
ViewData["Title"] = "블로그 | 백원숙 세무회계";
|
||||
}
|
||||
|
||||
<div class="container py-5">
|
||||
<h1 class="fw-bold mb-5">세무 블로그</h1>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="mb-4">
|
||||
<a href="/taxbaik/blog" class="btn btn-sm @(Model.SelectedCategoryId == null ? "btn-primary" : "btn-outline-primary")">전체</a>
|
||||
@foreach (var cat in Model.Categories)
|
||||
{
|
||||
<a href="/taxbaik/blog?categoryId=@cat.Id" class="btn btn-sm @(Model.SelectedCategoryId == cat.Id ? "btn-primary" : "btn-outline-primary")">@cat.Name</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Posts Grid -->
|
||||
<div class="row g-4">
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<div class="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>
|
||||
<h5 class="card-title mt-2">@post.Title</h5>
|
||||
<p class="card-text small">@post.CreatedAt.ToString("yyyy-MM-dd")</p>
|
||||
<a href="/taxbaik/blog/@post.Slug" class="btn btn-sm btn-primary">읽기</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<nav aria-label="Page navigation" class="mt-5">
|
||||
<ul class="pagination justify-content-center">
|
||||
@if (Model.CurrentPage > 1)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/taxbaik/blog?page=@(Model.CurrentPage - 1)">이전</a>
|
||||
</li>
|
||||
}
|
||||
@for (int i = 1; i <= Model.TotalPages; i++)
|
||||
{
|
||||
<li class="page-item @(i == Model.CurrentPage ? "active" : "")">
|
||||
<a class="page-link" href="/taxbaik/blog?page=@i">@i</a>
|
||||
</li>
|
||||
}
|
||||
@if (Model.CurrentPage < Model.TotalPages)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/taxbaik/blog?page=@(Model.CurrentPage + 1)">다음</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Blog;
|
||||
|
||||
public class BlogIndexModel : PageModel
|
||||
{
|
||||
private readonly BlogService _blogService;
|
||||
private readonly ICategoryRepository _categoryRepository;
|
||||
|
||||
public List<BlogPost> Posts { get; set; } = [];
|
||||
public List<Category> Categories { get; set; } = [];
|
||||
public int CurrentPage { get; set; } = 1;
|
||||
public int TotalPages { get; set; }
|
||||
public int? SelectedCategoryId { get; set; }
|
||||
private const int PageSize = 12;
|
||||
|
||||
public BlogIndexModel(BlogService blogService, ICategoryRepository categoryRepository)
|
||||
{
|
||||
_blogService = blogService;
|
||||
_categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync(int page = 1, int? categoryId = null)
|
||||
{
|
||||
CurrentPage = page;
|
||||
SelectedCategoryId = categoryId;
|
||||
Categories = (await _categoryRepository.GetAllAsync()).ToList();
|
||||
var (posts, total) = await _blogService.GetPublishedPagedAsync(page, PageSize, categoryId);
|
||||
Posts = posts.ToList();
|
||||
TotalPages = (total + PageSize - 1) / PageSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
@page "{slug}"
|
||||
@model BlogPostModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Post?.SeoTitle ?? Model.Post?.Title;
|
||||
ViewData["Description"] = Model.Post?.SeoDescription ?? "";
|
||||
ViewData["OgImage"] = Model.Post?.ThumbnailUrl ?? "";
|
||||
}
|
||||
|
||||
@if (Model.Post != null)
|
||||
{
|
||||
<article class="container py-5" style="max-width: 720px;">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/taxbaik">홈</a></li>
|
||||
<li class="breadcrumb-item"><a href="/taxbaik/blog">블로그</a></li>
|
||||
<li class="breadcrumb-item active">@Model.Post.CategoryName</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1 class="fw-bold mb-3">@Model.Post.Title</h1>
|
||||
<div class="text-muted small mb-4">
|
||||
<span>@Model.Post.CreatedAt.ToString("yyyy-MM-dd")</span>
|
||||
<span class="ms-3">👁️ @Model.Post.ViewCount</span>
|
||||
<span class="badge bg-primary ms-3">@Model.Post.CategoryName</span>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="article-body">
|
||||
@Html.Raw(Model.Post.Content)
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="bg-light p-4 rounded mb-5">
|
||||
<h5 class="fw-bold mb-2">상담이 필요하신가요?</h5>
|
||||
<p class="mb-3">이 글과 관련된 상담이 필요하면 언제든 연락주세요.</p>
|
||||
<a href="/taxbaik/contact" class="btn btn-primary">상담 신청하기</a>
|
||||
</div>
|
||||
|
||||
<!-- Share -->
|
||||
<div class="text-center mb-5">
|
||||
<p class="small text-muted">이 글을 공유하세요:</p>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="copyUrl()">📋 링크복사</button>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="container py-5 text-center">
|
||||
<p class="fs-5">포스트를 찾을 수 없습니다.</p>
|
||||
<a href="/taxbaik/blog" class="btn btn-primary">블로그 돌아가기</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<script>
|
||||
function copyUrl() {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
alert('링크가 복사되었습니다.');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Blog;
|
||||
|
||||
public class BlogPostModel : PageModel
|
||||
{
|
||||
private readonly BlogService _blogService;
|
||||
|
||||
public BlogPost? Post { get; set; }
|
||||
|
||||
public BlogPostModel(BlogService blogService)
|
||||
{
|
||||
_blogService = blogService;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync(string slug)
|
||||
{
|
||||
Post = await _blogService.GetBySlugAsync(slug);
|
||||
if (Post != null)
|
||||
{
|
||||
_ = _blogService.IncrementViewCountAsync(Post.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user