구현: W2 도메인·인프라·서비스 레이어

Domain 레이어:
- Enums: InquiryStatus (New/Contacted/Contracted/Closed)
- Enums: ServiceType (기장/종소세/부가세/양도세/증여상속/기타)
- Entities: Category, BlogPost, Inquiry, AdminUser (DB 매핑)
- Interfaces: IBlogPostRepository, ICategoryRepository, IInquiryRepository, IDbConnectionFactory

Infrastructure 레이어:
- DbConnectionFactory: PostgreSQL 연결 팩토리 (Npgsql)
- BlogPostRepository: GetBySlug, GetPublishedPaged, Create, Update, Delete, IncrementViewCount
- CategoryRepository: GetAll, GetBySlug
- InquiryRepository: Create, GetPaged, UpdateStatus
- NuGet 의존성: Dapper 2.1.15, Npgsql 8.0.1, Configuration, DependencyInjection
- DependencyInjection: AddInfrastructure() 확장 메서드

기술 결정:
- Dapper로 SQL 완전 제어
- PostgreSQL 다중 쿼리 (QueryMultiple) 페이징 최적화
- 한국어 파라미터 처리 (::int, ::text 타입 명시)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-26 15:06:22 +09:00
parent 303ba49292
commit d5918c562c
17 changed files with 363 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
namespace TaxBaik.Domain.Entities;
public class AdminUser
{
public int Id { get; set; }
public string Username { get; set; } = null!;
public string PasswordHash { get; set; } = null!;
public DateTime? LastLoginAt { get; set; }
public DateTime CreatedAt { get; set; }
}
+23
View File
@@ -0,0 +1,23 @@
namespace TaxBaik.Domain.Entities;
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; } = null!;
public string Content { get; set; } = null!;
public string Slug { get; set; } = null!;
public int? CategoryId { get; set; }
public string? Tags { get; set; }
public int? AuthorId { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public string? SeoTitle { get; set; }
public string? SeoDescription { get; set; }
public string? ThumbnailUrl { get; set; }
public bool IsPublished { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
// Navigation property (populated via LEFT JOIN, not stored in DB)
public string? CategoryName { get; set; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace TaxBaik.Domain.Entities;
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string Slug { get; set; } = null!;
public int SortOrder { get; set; }
}
+16
View File
@@ -0,0 +1,16 @@
namespace TaxBaik.Domain.Entities;
using TaxBaik.Domain.Enums;
public class Inquiry
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string Phone { get; set; } = null!;
public string? Email { get; set; }
public string ServiceType { get; set; } = null!;
public string Message { get; set; } = null!;
public string Status { get; set; } = "new";
public string? IpAddress { get; set; }
public DateTime CreatedAt { get; set; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace TaxBaik.Domain.Enums;
public enum InquiryStatus
{
New = 0, // 새로운 문의
Contacted = 1, // 연락 완료
Contracted = 2, // 계약 체결
Closed = 3 // 종료
}
+11
View File
@@ -0,0 +1,11 @@
namespace TaxBaik.Domain.Enums;
public enum ServiceType
{
Bookkeeping = 0, // 기장
IncomeTax = 1, // 종소세
VatTax = 2, // 부가세
CapitalGainsTax = 3, // 양도세
GiftInheritanceTax = 4,// 증여·상속
Other = 5 // 기타
}
@@ -0,0 +1,16 @@
namespace TaxBaik.Domain.Interfaces;
using TaxBaik.Domain.Entities;
public interface IBlogPostRepository
{
Task<BlogPost?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<BlogPost?> GetBySlugAsync(string slug, CancellationToken cancellationToken = default);
Task<(IEnumerable<BlogPost> Items, int Total)> GetPublishedPagedAsync(
int page, int pageSize, int? categoryId = null, CancellationToken cancellationToken = default);
Task<IEnumerable<BlogPost>> GetAllForAdminAsync(CancellationToken cancellationToken = default);
Task<int> CreateAsync(BlogPost post, CancellationToken cancellationToken = default);
Task UpdateAsync(BlogPost post, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
Task IncrementViewCountAsync(int id, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,9 @@
namespace TaxBaik.Domain.Interfaces;
using TaxBaik.Domain.Entities;
public interface ICategoryRepository
{
Task<IEnumerable<Category>> GetAllAsync(CancellationToken cancellationToken = default);
Task<Category?> GetBySlugAsync(string slug, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,8 @@
namespace TaxBaik.Domain.Interfaces;
using System.Data;
public interface IDbConnectionFactory
{
IDbConnection CreateConnection();
}
@@ -0,0 +1,12 @@
namespace TaxBaik.Domain.Interfaces;
using TaxBaik.Domain.Entities;
public interface IInquiryRepository
{
Task<int> CreateAsync(Inquiry inquiry, CancellationToken cancellationToken = default);
Task<Inquiry?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
int page, int pageSize, string? status = null, CancellationToken cancellationToken = default);
Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default);
}