feat: 공지사항 목록/상세 API 구현 (#78)
This commit is contained in:
parent
c8c9a44b0f
commit
f0d325fda9
44
SPMS.API/Controllers/NoticeController.cs
Normal file
44
SPMS.API/Controllers/NoticeController.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using SPMS.Application.DTOs.Notice;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
|
||||
namespace SPMS.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/in/public/notice")]
|
||||
[ApiExplorerSettings(GroupName = "public")]
|
||||
public class NoticeController : ControllerBase
|
||||
{
|
||||
private readonly INoticeService _noticeService;
|
||||
|
||||
public NoticeController(INoticeService noticeService)
|
||||
{
|
||||
_noticeService = noticeService;
|
||||
}
|
||||
|
||||
[HttpPost("list")]
|
||||
[SwaggerOperation(Summary = "공지사항 목록", Description = "활성화된 공지사항 목록을 페이징으로 조회합니다. 상단 고정 우선 정렬.")]
|
||||
public async Task<IActionResult> GetListAsync([FromBody] NoticeListRequestDto request)
|
||||
{
|
||||
var serviceCode = Request.Headers["X-Service-Code"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(serviceCode))
|
||||
return BadRequest(ApiResponse.Fail(ErrorCodes.BadRequest, "X-Service-Code 헤더가 필요합니다."));
|
||||
|
||||
var result = await _noticeService.GetListAsync(serviceCode, request);
|
||||
return Ok(ApiResponse<NoticeListResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("info")]
|
||||
[SwaggerOperation(Summary = "공지사항 상세", Description = "공지사항 ID로 상세 정보를 조회합니다.")]
|
||||
public async Task<IActionResult> GetInfoAsync([FromBody] NoticeInfoRequestDto request)
|
||||
{
|
||||
var serviceCode = Request.Headers["X-Service-Code"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(serviceCode))
|
||||
return BadRequest(ApiResponse.Fail(ErrorCodes.BadRequest, "X-Service-Code 헤더가 필요합니다."));
|
||||
|
||||
var result = await _noticeService.GetInfoAsync(serviceCode, request);
|
||||
return Ok(ApiResponse<NoticeInfoResponseDto>.Success(result));
|
||||
}
|
||||
}
|
||||
11
SPMS.Application/DTOs/Notice/NoticeInfoRequestDto.cs
Normal file
11
SPMS.Application/DTOs/Notice/NoticeInfoRequestDto.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notice;
|
||||
|
||||
public class NoticeInfoRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "공지사항 ID는 필수입니다.")]
|
||||
[JsonPropertyName("notice_id")]
|
||||
public long NoticeId { get; set; }
|
||||
}
|
||||
21
SPMS.Application/DTOs/Notice/NoticeInfoResponseDto.cs
Normal file
21
SPMS.Application/DTOs/Notice/NoticeInfoResponseDto.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notice;
|
||||
|
||||
public class NoticeInfoResponseDto
|
||||
{
|
||||
[JsonPropertyName("notice_id")]
|
||||
public long NoticeId { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("is_important")]
|
||||
public bool IsImportant { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
12
SPMS.Application/DTOs/Notice/NoticeListRequestDto.cs
Normal file
12
SPMS.Application/DTOs/Notice/NoticeListRequestDto.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notice;
|
||||
|
||||
public class NoticeListRequestDto
|
||||
{
|
||||
[Range(1, int.MaxValue, ErrorMessage = "페이지 번호는 1 이상이어야 합니다.")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[Range(1, 100, ErrorMessage = "페이지 크기는 1~100 사이여야 합니다.")]
|
||||
public int Size { get; set; } = 20;
|
||||
}
|
||||
42
SPMS.Application/DTOs/Notice/NoticeListResponseDto.cs
Normal file
42
SPMS.Application/DTOs/Notice/NoticeListResponseDto.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notice;
|
||||
|
||||
public class NoticeListResponseDto
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public List<NoticeSummaryDto> Items { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("pagination")]
|
||||
public PaginationDto Pagination { get; set; } = new();
|
||||
}
|
||||
|
||||
public class NoticeSummaryDto
|
||||
{
|
||||
[JsonPropertyName("notice_id")]
|
||||
public long NoticeId { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("is_important")]
|
||||
public bool IsImportant { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class PaginationDto
|
||||
{
|
||||
[JsonPropertyName("page")]
|
||||
public int Page { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int Size { get; set; }
|
||||
|
||||
[JsonPropertyName("total_count")]
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
[JsonPropertyName("total_pages")]
|
||||
public int TotalPages { get; set; }
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ public static class DependencyInjection
|
|||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<IServiceManagementService, ServiceManagementService>();
|
||||
services.AddScoped<INoticeService, NoticeService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
9
SPMS.Application/Interfaces/INoticeService.cs
Normal file
9
SPMS.Application/Interfaces/INoticeService.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using SPMS.Application.DTOs.Notice;
|
||||
|
||||
namespace SPMS.Application.Interfaces;
|
||||
|
||||
public interface INoticeService
|
||||
{
|
||||
Task<NoticeListResponseDto> GetListAsync(string serviceCode, NoticeListRequestDto request);
|
||||
Task<NoticeInfoResponseDto> GetInfoAsync(string serviceCode, NoticeInfoRequestDto request);
|
||||
}
|
||||
68
SPMS.Application/Services/NoticeService.cs
Normal file
68
SPMS.Application/Services/NoticeService.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using SPMS.Application.DTOs.Notice;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Domain.Exceptions;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Application.Services;
|
||||
|
||||
public class NoticeService : INoticeService
|
||||
{
|
||||
private readonly INoticeRepository _noticeRepository;
|
||||
private readonly IServiceRepository _serviceRepository;
|
||||
|
||||
public NoticeService(INoticeRepository noticeRepository, IServiceRepository serviceRepository)
|
||||
{
|
||||
_noticeRepository = noticeRepository;
|
||||
_serviceRepository = serviceRepository;
|
||||
}
|
||||
|
||||
public async Task<NoticeListResponseDto> GetListAsync(string serviceCode, NoticeListRequestDto request)
|
||||
{
|
||||
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||
if (service == null)
|
||||
throw new SpmsException(ErrorCodes.NotFound, "존재하지 않는 서비스입니다.", 404);
|
||||
|
||||
var (items, totalCount) = await _noticeRepository.GetActivePagedAsync(service.Id, request.Page, request.Size);
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
||||
|
||||
return new NoticeListResponseDto
|
||||
{
|
||||
Items = items.Select(n => new NoticeSummaryDto
|
||||
{
|
||||
NoticeId = n.Id,
|
||||
Title = n.Title,
|
||||
IsImportant = n.IsPinned,
|
||||
CreatedAt = n.CreatedAt
|
||||
}).ToList(),
|
||||
Pagination = new PaginationDto
|
||||
{
|
||||
Page = request.Page,
|
||||
Size = request.Size,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NoticeInfoResponseDto> GetInfoAsync(string serviceCode, NoticeInfoRequestDto request)
|
||||
{
|
||||
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||
if (service == null)
|
||||
throw new SpmsException(ErrorCodes.NotFound, "존재하지 않는 서비스입니다.", 404);
|
||||
|
||||
var notice = await _noticeRepository.GetActiveByIdAsync(request.NoticeId, service.Id);
|
||||
if (notice == null)
|
||||
throw new SpmsException(ErrorCodes.NotFound, "존재하지 않는 공지사항입니다.", 404);
|
||||
|
||||
return new NoticeInfoResponseDto
|
||||
{
|
||||
NoticeId = notice.Id,
|
||||
Title = notice.Title,
|
||||
Content = notice.Content,
|
||||
IsImportant = notice.IsPinned,
|
||||
CreatedAt = notice.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
9
SPMS.Domain/Interfaces/INoticeRepository.cs
Normal file
9
SPMS.Domain/Interfaces/INoticeRepository.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using SPMS.Domain.Entities;
|
||||
|
||||
namespace SPMS.Domain.Interfaces;
|
||||
|
||||
public interface INoticeRepository : IRepository<Notice>
|
||||
{
|
||||
Task<(IReadOnlyList<Notice> Items, int TotalCount)> GetActivePagedAsync(long serviceId, int page, int size);
|
||||
Task<Notice?> GetActiveByIdAsync(long id, long serviceId);
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ public static class DependencyInjection
|
|||
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
services.AddScoped<IServiceRepository, ServiceRepository>();
|
||||
services.AddScoped<IAdminRepository, AdminRepository>();
|
||||
services.AddScoped<INoticeRepository, NoticeRepository>();
|
||||
|
||||
// External Services
|
||||
services.AddScoped<IJwtService, JwtService>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class NoticeRepository : Repository<Notice>, INoticeRepository
|
||||
{
|
||||
public NoticeRepository(AppDbContext context) : base(context) { }
|
||||
|
||||
public async Task<(IReadOnlyList<Notice> Items, int TotalCount)> GetActivePagedAsync(long serviceId, int page, int size)
|
||||
{
|
||||
var query = _dbSet.Where(n => n.ServiceId == serviceId && n.IsActive);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(n => n.IsPinned)
|
||||
.ThenByDescending(n => n.CreatedAt)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<Notice?> GetActiveByIdAsync(long id, long serviceId)
|
||||
{
|
||||
return await _dbSet.FirstOrDefaultAsync(n => n.Id == id && n.ServiceId == serviceId && n.IsActive);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user