feat: 공지사항 목록/상세 API 구현 (#78)
All checks were successful
SPMS_API/pipeline/head This commit looks good
All checks were successful
SPMS_API/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/79
This commit is contained in:
commit
f5a1afc6b3
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<IAuthService, AuthService>();
|
||||||
services.AddScoped<IAccountService, AccountService>();
|
services.AddScoped<IAccountService, AccountService>();
|
||||||
services.AddScoped<IServiceManagementService, ServiceManagementService>();
|
services.AddScoped<IServiceManagementService, ServiceManagementService>();
|
||||||
|
services.AddScoped<INoticeService, NoticeService>();
|
||||||
|
|
||||||
return services;
|
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(typeof(IRepository<>), typeof(Repository<>));
|
||||||
services.AddScoped<IServiceRepository, ServiceRepository>();
|
services.AddScoped<IServiceRepository, ServiceRepository>();
|
||||||
services.AddScoped<IAdminRepository, AdminRepository>();
|
services.AddScoped<IAdminRepository, AdminRepository>();
|
||||||
|
services.AddScoped<INoticeRepository, NoticeRepository>();
|
||||||
|
|
||||||
// External Services
|
// External Services
|
||||||
services.AddScoped<IJwtService, JwtService>();
|
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