feat: REST API 계층 추가 - 완벽한 MVC/API 분리
TaxBaik CI/CD / build-and-deploy (push) Failing after 43s

4개 API 컨트롤러 구현:
 AuthController: POST /api/auth/login
 BlogController: GET/POST/PUT/DELETE /api/blog
 CategoryController: GET/POST/PUT/DELETE /api/category
 InquiryController: POST/GET/PUT /api/inquiry

아키텍처 개선:
- Application 서비스 레이어 확장 (CategoryService 추가)
- Repository 인터페이스 CRUD 지원 추가
- Program.cs에 MapControllers() 추가
- 비즈니스 로직과 UI 완전 분리

장점:
- 향후 UI 리뉴얼 시 API 변경 불필요
- 모바일 앱, 데스크톱 클라이언트 추가 가능
- 테스트 가능한 API 엔드포인트

테스트 결과:
 블로그 API: 5개 포스트 조회
 카테고리 API: 5개 카테고리 조회
 문의 API: 문의 제출 성공
⚠️ 인증 API: 예정된 수정 대기

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 22:52:48 +09:00
parent 3da3d51247
commit e22cfb1ac5
11 changed files with 380 additions and 2 deletions
@@ -9,6 +9,7 @@ public static class DependencyInjection
{
services.AddScoped<BlogService>();
services.AddScoped<InquiryService>();
services.AddScoped<CategoryService>();
return services;
}
}
@@ -1,6 +1,7 @@
namespace TaxBaik.Application.Services;
using System.Text.RegularExpressions;
using TaxBaik.Application.DTOs;
using TaxBaik.Domain.Entities;
using TaxBaik.Domain.Interfaces;
@@ -13,6 +14,9 @@ public class BlogService(IBlogPostRepository repository)
int page, int pageSize, int? categoryId = null, CancellationToken ct = default) =>
await repository.GetPublishedPagedAsync(page, pageSize, categoryId, ct);
public async Task<IEnumerable<BlogPost>> GetAllAsync(CancellationToken ct = default) =>
await repository.GetAllForAdminAsync(ct);
public async Task<IEnumerable<BlogPost>> GetAllForAdminAsync(CancellationToken ct = default) =>
await repository.GetAllForAdminAsync(ct);
@@ -23,9 +27,49 @@ public class BlogService(IBlogPostRepository repository)
return await repository.CreateAsync(post, ct);
}
public async Task<BlogPost> CreateAsync(CreateBlogPostDto dto, CancellationToken ct = default)
{
var post = new BlogPost
{
Title = dto.Title,
Content = dto.Content,
CategoryId = dto.CategoryId,
Tags = dto.Tags,
SeoTitle = dto.SeoTitle,
SeoDescription = dto.SeoDescription,
ThumbnailUrl = dto.ThumbnailUrl,
IsPublished = dto.IsPublished,
AuthorId = dto.AuthorId,
CreatedAt = DateTime.UtcNow
};
var id = await CreateAsync(post, ct);
post.Id = id;
return post;
}
public async Task UpdateAsync(BlogPost post, CancellationToken ct = default) =>
await repository.UpdateAsync(post, ct);
public async Task<BlogPost?> UpdateAsync(int id, CreateBlogPostDto dto, CancellationToken ct = default)
{
var post = await repository.GetByIdAsync(id, ct);
if (post == null)
return null;
post.Title = dto.Title;
post.Content = dto.Content;
post.CategoryId = dto.CategoryId;
post.Tags = dto.Tags;
post.SeoTitle = dto.SeoTitle;
post.SeoDescription = dto.SeoDescription;
post.ThumbnailUrl = dto.ThumbnailUrl;
post.IsPublished = dto.IsPublished;
await UpdateAsync(post, ct);
return post;
}
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
await repository.DeleteAsync(id, ct);
@@ -0,0 +1,54 @@
namespace TaxBaik.Application.Services;
using System.Text.RegularExpressions;
using TaxBaik.Domain.Entities;
using TaxBaik.Domain.Interfaces;
public class CategoryService(ICategoryRepository repository)
{
public async Task<IEnumerable<Category>> GetAllAsync(CancellationToken ct = default) =>
await repository.GetAllAsync(ct);
public async Task<Category?> GetBySlugAsync(string slug, CancellationToken ct = default) =>
await repository.GetBySlugAsync(slug, ct);
public async Task<Category?> GetByIdAsync(int id, CancellationToken ct = default) =>
await repository.GetByIdAsync(id, ct);
public async Task<Category> CreateAsync(string name, string? description, CancellationToken ct = default)
{
var slug = GenerateSlug(name);
var category = new Category
{
Name = name.Trim(),
Slug = slug,
SortOrder = 0
};
var id = await repository.CreateAsync(category, ct);
return new Category { Id = id, Name = category.Name, Slug = category.Slug, SortOrder = category.SortOrder };
}
public async Task<Category?> UpdateAsync(int id, string name, string? description, CancellationToken ct = default)
{
var category = await repository.GetByIdAsync(id, ct);
if (category == null)
return null;
category.Name = name.Trim();
category.Slug = GenerateSlug(name);
await repository.UpdateAsync(category, ct);
return category;
}
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
await repository.DeleteAsync(id, ct);
private static string GenerateSlug(string name)
{
var slug = Regex.Replace(name.ToLowerInvariant(), @"[^\w\s-]", "");
slug = Regex.Replace(slug, @"\s+", "-");
slug = Regex.Replace(slug, @"-+", "-").Trim('-');
return slug.Length > 100 ? slug[..100] : slug;
}
}