improvement: 메시지 목록 확장 (#224) #225
|
|
@ -32,10 +32,10 @@ public class MessageController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("list")]
|
[HttpPost("list")]
|
||||||
[SwaggerOperation(Summary = "메시지 목록 조회", Description = "서비스별 메시지 목록을 페이지 단위로 조회합니다.")]
|
[SwaggerOperation(Summary = "메시지 목록 조회", Description = "메시지 목록을 페이지 단위로 조회합니다. X-Service-Code 헤더가 있으면 해당 서비스만, 없으면 전체 서비스 메시지를 반환합니다. send_status 필터(complete/pending/failed)를 지원합니다.")]
|
||||||
public async Task<IActionResult> GetListAsync([FromBody] MessageListRequestDto request)
|
public async Task<IActionResult> GetListAsync([FromBody] MessageListRequestDto request)
|
||||||
{
|
{
|
||||||
var serviceId = GetServiceId();
|
var serviceId = GetServiceIdOrNull();
|
||||||
var result = await _messageService.GetListAsync(serviceId, request);
|
var result = await _messageService.GetListAsync(serviceId, request);
|
||||||
return Ok(ApiResponse<MessageListResponseDto>.Success(result));
|
return Ok(ApiResponse<MessageListResponseDto>.Success(result));
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +83,13 @@ public class MessageController : ControllerBase
|
||||||
throw new SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
|
throw new SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long? GetServiceIdOrNull()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
|
||||||
|
return serviceId;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private long GetAdminId()
|
private long GetAdminId()
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ public class ServiceCodeMiddleware
|
||||||
|
|
||||||
// === OPTIONAL_FOR_ADMIN: 관리자는 X-Service-Code 선택 ===
|
// === OPTIONAL_FOR_ADMIN: 관리자는 X-Service-Code 선택 ===
|
||||||
if (path.StartsWithSegments("/v1/in/stats") ||
|
if (path.StartsWithSegments("/v1/in/stats") ||
|
||||||
path.StartsWithSegments("/v1/in/device/list"))
|
path.StartsWithSegments("/v1/in/device/list") ||
|
||||||
|
path.StartsWithSegments("/v1/in/message/list"))
|
||||||
{
|
{
|
||||||
if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) &&
|
if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) &&
|
||||||
!string.IsNullOrWhiteSpace(optionalCode))
|
!string.IsNullOrWhiteSpace(optionalCode))
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,10 @@ public class MessageListRequestDto
|
||||||
|
|
||||||
[JsonPropertyName("is_active")]
|
[JsonPropertyName("is_active")]
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("service_code")]
|
||||||
|
public string? ServiceCode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("send_status")]
|
||||||
|
public string? SendStatus { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,13 @@ public class MessageSummaryDto
|
||||||
|
|
||||||
[JsonPropertyName("created_at")]
|
[JsonPropertyName("created_at")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("service_name")]
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("service_code")]
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("latest_send_status")]
|
||||||
|
public string LatestSendStatus { get; set; } = "pending";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace SPMS.Application.Interfaces;
|
||||||
public interface IMessageService
|
public interface IMessageService
|
||||||
{
|
{
|
||||||
Task<MessageSaveResponseDto> SaveAsync(long serviceId, long adminId, MessageSaveRequestDto request);
|
Task<MessageSaveResponseDto> SaveAsync(long serviceId, long adminId, MessageSaveRequestDto request);
|
||||||
Task<MessageListResponseDto> GetListAsync(long serviceId, MessageListRequestDto request);
|
Task<MessageListResponseDto> GetListAsync(long? serviceId, MessageListRequestDto request);
|
||||||
Task<MessageInfoResponseDto> GetInfoAsync(long serviceId, MessageInfoRequestDto request);
|
Task<MessageInfoResponseDto> GetInfoAsync(long serviceId, MessageInfoRequestDto request);
|
||||||
Task DeleteAsync(long serviceId, MessageDeleteRequestDto request);
|
Task DeleteAsync(long serviceId, MessageDeleteRequestDto request);
|
||||||
Task<MessagePreviewResponseDto> PreviewAsync(long serviceId, MessagePreviewRequestDto request);
|
Task<MessagePreviewResponseDto> PreviewAsync(long serviceId, MessagePreviewRequestDto request);
|
||||||
|
|
|
||||||
|
|
@ -63,26 +63,28 @@ public class MessageService : IMessageService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MessageListResponseDto> GetListAsync(long serviceId, MessageListRequestDto request)
|
public async Task<MessageListResponseDto> GetListAsync(long? serviceId, MessageListRequestDto request)
|
||||||
{
|
{
|
||||||
var page = Math.Max(1, request.Page);
|
var page = Math.Max(1, request.Page);
|
||||||
var size = Math.Clamp(request.Size, 1, 100);
|
var size = Math.Clamp(request.Size, 1, 100);
|
||||||
|
|
||||||
var (items, totalCount) = await _messageRepository.GetPagedByServiceAsync(
|
var (items, totalCount) = await _messageRepository.GetPagedForListAsync(
|
||||||
serviceId, page, size,
|
serviceId, page, size,
|
||||||
m => (request.Keyword == null || m.Title.Contains(request.Keyword) || m.Body.Contains(request.Keyword))
|
request.Keyword, request.IsActive, request.SendStatus);
|
||||||
&& (request.IsActive == null || m.IsDeleted != request.IsActive));
|
|
||||||
|
|
||||||
var totalPages = (int)Math.Ceiling((double)totalCount / size);
|
var totalPages = (int)Math.Ceiling((double)totalCount / size);
|
||||||
|
|
||||||
return new MessageListResponseDto
|
return new MessageListResponseDto
|
||||||
{
|
{
|
||||||
Items = items.Select(m => new MessageSummaryDto
|
Items = items.Select(p => new MessageSummaryDto
|
||||||
{
|
{
|
||||||
MessageCode = m.MessageCode,
|
MessageCode = p.MessageCode,
|
||||||
Title = m.Title,
|
Title = p.Title,
|
||||||
IsActive = !m.IsDeleted,
|
IsActive = p.IsActive,
|
||||||
CreatedAt = m.CreatedAt
|
CreatedAt = p.CreatedAt,
|
||||||
|
ServiceName = p.ServiceName,
|
||||||
|
ServiceCode = p.ServiceCode,
|
||||||
|
LatestSendStatus = DetermineSendStatus(p.TotalSendCount, p.SuccessCount)
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
Pagination = new PaginationDto
|
Pagination = new PaginationDto
|
||||||
{
|
{
|
||||||
|
|
@ -94,6 +96,12 @@ public class MessageService : IMessageService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string DetermineSendStatus(int totalSend, int successCount)
|
||||||
|
{
|
||||||
|
if (totalSend == 0) return "pending";
|
||||||
|
return successCount > 0 ? "complete" : "failed";
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<MessageInfoResponseDto> GetInfoAsync(long serviceId, MessageInfoRequestDto request)
|
public async Task<MessageInfoResponseDto> GetInfoAsync(long serviceId, MessageInfoRequestDto request)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.MessageCode))
|
if (string.IsNullOrWhiteSpace(request.MessageCode))
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,19 @@ public interface IMessageRepository : IRepository<Message>
|
||||||
Task<(IReadOnlyList<Message> Items, int TotalCount)> GetPagedByServiceAsync(
|
Task<(IReadOnlyList<Message> Items, int TotalCount)> GetPagedByServiceAsync(
|
||||||
long serviceId, int page, int size,
|
long serviceId, int page, int size,
|
||||||
Expression<Func<Message, bool>>? predicate = null);
|
Expression<Func<Message, bool>>? predicate = null);
|
||||||
|
Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync(
|
||||||
|
long? serviceId, int page, int size,
|
||||||
|
string? keyword = null, bool? isActive = null, string? sendStatus = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageListProjection
|
||||||
|
{
|
||||||
|
public string MessageCode { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
public int TotalSendCount { get; set; }
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SPMS.Domain.Entities;
|
using SPMS.Domain.Entities;
|
||||||
|
using SPMS.Domain.Enums;
|
||||||
using SPMS.Domain.Interfaces;
|
using SPMS.Domain.Interfaces;
|
||||||
|
|
||||||
namespace SPMS.Infrastructure.Persistence.Repositories;
|
namespace SPMS.Infrastructure.Persistence.Repositories;
|
||||||
|
|
@ -52,4 +53,50 @@ public class MessageRepository : Repository<Message>, IMessageRepository
|
||||||
|
|
||||||
return (items, totalCount);
|
return (items, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync(
|
||||||
|
long? serviceId, int page, int size,
|
||||||
|
string? keyword = null, bool? isActive = null, string? sendStatus = null)
|
||||||
|
{
|
||||||
|
var query = _dbSet.Where(m => !m.IsDeleted);
|
||||||
|
|
||||||
|
if (serviceId.HasValue)
|
||||||
|
query = query.Where(m => m.ServiceId == serviceId.Value);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
query = query.Where(m => m.Title.Contains(keyword) || m.Body.Contains(keyword));
|
||||||
|
|
||||||
|
if (isActive.HasValue)
|
||||||
|
query = query.Where(m => m.IsDeleted != isActive.Value);
|
||||||
|
|
||||||
|
var projected = query.Select(m => new MessageListProjection
|
||||||
|
{
|
||||||
|
MessageCode = m.MessageCode,
|
||||||
|
Title = m.Title,
|
||||||
|
IsActive = !m.IsDeleted,
|
||||||
|
CreatedAt = m.CreatedAt,
|
||||||
|
ServiceName = m.Service.ServiceName,
|
||||||
|
ServiceCode = m.Service.ServiceCode,
|
||||||
|
TotalSendCount = _context.PushSendLogs.Count(l => l.MessageId == m.Id),
|
||||||
|
SuccessCount = _context.PushSendLogs.Count(l => l.MessageId == m.Id && l.Status == PushResult.Success)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 발송 상태 필터
|
||||||
|
if (sendStatus == "complete")
|
||||||
|
projected = projected.Where(p => p.SuccessCount > 0);
|
||||||
|
else if (sendStatus == "failed")
|
||||||
|
projected = projected.Where(p => p.TotalSendCount > 0 && p.SuccessCount == 0);
|
||||||
|
else if (sendStatus == "pending")
|
||||||
|
projected = projected.Where(p => p.TotalSendCount == 0);
|
||||||
|
|
||||||
|
var totalCount = await projected.CountAsync();
|
||||||
|
|
||||||
|
var items = await projected
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * size)
|
||||||
|
.Take(size)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return (items, totalCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user