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
+35
View File
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly AuthService _authService;
public AuthController(AuthService authService)
{
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
return BadRequest(new { message = "Username and password are required" });
var token = await _authService.AuthenticateAndGenerateTokenAsync(request.Username, request.Password);
if (token == null)
return Unauthorized(new { message = "Invalid username or password" });
return Ok(new { token, expiresIn = 28800 });
}
}
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
+71
View File
@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Application.Services;
using TaxBaik.Application.DTOs;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BlogController : ControllerBase
{
private readonly BlogService _blogService;
public BlogController(BlogService blogService)
{
_blogService = blogService;
}
[HttpGet]
public async Task<IActionResult> GetPublished([FromQuery] int page = 1, [FromQuery] int pageSize = 12)
{
var (items, total) = await _blogService.GetPublishedPagedAsync(page, pageSize);
return Ok(new { data = items, total, page, pageSize });
}
[HttpGet("{slug}")]
public async Task<IActionResult> GetBySlug(string slug)
{
var post = await _blogService.GetBySlugAsync(slug);
if (post == null)
return NotFound(new { message = "Post not found" });
return Ok(post);
}
[HttpGet("admin/all")]
[Authorize]
public async Task<IActionResult> GetAll()
{
var posts = await _blogService.GetAllAsync();
return Ok(posts);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Create([FromBody] CreateBlogPostDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Title) || string.IsNullOrWhiteSpace(dto.Content))
return BadRequest(new { message = "Title and content are required" });
var result = await _blogService.CreateAsync(dto);
return CreatedAtAction(nameof(GetBySlug), new { slug = result.Slug }, result);
}
[HttpPut("{id}")]
[Authorize]
public async Task<IActionResult> Update(int id, [FromBody] CreateBlogPostDto dto)
{
var result = await _blogService.UpdateAsync(id, dto);
if (result == null)
return NotFound(new { message = "Post not found" });
return Ok(result);
}
[HttpDelete("{id}")]
[Authorize]
public async Task<IActionResult> Delete(int id)
{
await _blogService.DeleteAsync(id);
return NoContent();
}
}
@@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class CategoryController : ControllerBase
{
private readonly CategoryService _categoryService;
public CategoryController(CategoryService categoryService)
{
_categoryService = categoryService;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var categories = await _categoryService.GetAllAsync();
return Ok(categories);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Create([FromBody] CreateCategoryRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
return BadRequest(new { message = "Category name is required" });
var category = await _categoryService.CreateAsync(request.Name, request.Description);
return CreatedAtAction(nameof(GetAll), category);
}
[HttpPut("{id}")]
[Authorize]
public async Task<IActionResult> Update(int id, [FromBody] CreateCategoryRequest request)
{
var category = await _categoryService.UpdateAsync(id, request.Name, request.Description);
if (category == null)
return NotFound(new { message = "Category not found" });
return Ok(category);
}
[HttpDelete("{id}")]
[Authorize]
public async Task<IActionResult> Delete(int id)
{
await _categoryService.DeleteAsync(id);
return NoContent();
}
}
public class CreateCategoryRequest
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Interfaces;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class InquiryController : ControllerBase
{
private readonly InquiryService _inquiryService;
private readonly IInquiryRepository _inquiryRepository;
public InquiryController(InquiryService inquiryService, IInquiryRepository inquiryRepository)
{
_inquiryService = inquiryService;
_inquiryRepository = inquiryRepository;
}
[HttpPost]
public async Task<IActionResult> Submit([FromBody] SubmitInquiryRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Phone))
return BadRequest(new { message = "Name and phone are required" });
await _inquiryService.SubmitAsync(request.Name, request.Phone, request.ServiceType, request.Message);
return Ok(new { message = "Inquiry submitted successfully" });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
var (inquiries, total) = await _inquiryRepository.GetPagedAsync(page, pageSize);
return Ok(new { data = inquiries, total, page, pageSize });
}
[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> GetById(int id)
{
var inquiry = await _inquiryRepository.GetByIdAsync(id);
if (inquiry == null)
return NotFound(new { message = "Inquiry not found" });
return Ok(inquiry);
}
[HttpPut("{id}/status")]
[Authorize]
public async Task<IActionResult> UpdateStatus(int id, [FromBody] UpdateStatusRequest request)
{
var inquiry = await _inquiryRepository.GetByIdAsync(id);
if (inquiry == null)
return NotFound(new { message = "Inquiry not found" });
await _inquiryRepository.UpdateStatusAsync(id, request.Status);
return Ok(new { message = "Status updated" });
}
}
public class SubmitInquiryRequest
{
public string Name { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string? Email { get; set; }
public string ServiceType { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public class UpdateStatusRequest
{
public string Status { get; set; } = string.Empty;
}