improvement: 대시보드 통합 API 추가 (#231) #232
|
|
@ -82,6 +82,15 @@ public class StatsController : ControllerBase
|
||||||
return Ok(ApiResponse<FailureStatResponseDto>.Success(result, "조회 성공"));
|
return Ok(ApiResponse<FailureStatResponseDto>.Success(result, "조회 성공"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("dashboard")]
|
||||||
|
[SwaggerOperation(Summary = "대시보드 통합 조회", Description = "KPI, 일별 추이, 시간대별 분포, 플랫폼 비율, 상위 메시지를 한번에 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")]
|
||||||
|
public async Task<IActionResult> GetDashboardAsync([FromBody] DashboardRequestDto request)
|
||||||
|
{
|
||||||
|
var serviceId = GetOptionalServiceId();
|
||||||
|
var result = await _statsService.GetDashboardAsync(serviceId, request);
|
||||||
|
return Ok(ApiResponse<DashboardResponseDto>.Success(result, "조회 성공"));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("send-log")]
|
[HttpPost("send-log")]
|
||||||
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")]
|
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")]
|
||||||
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
|
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
|
||||||
|
|
|
||||||
12
SPMS.Application/DTOs/Stats/DashboardRequestDto.cs
Normal file
12
SPMS.Application/DTOs/Stats/DashboardRequestDto.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Stats;
|
||||||
|
|
||||||
|
public class DashboardRequestDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("start_date")]
|
||||||
|
public string StartDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("end_date")]
|
||||||
|
public string EndDate { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
72
SPMS.Application/DTOs/Stats/DashboardResponseDto.cs
Normal file
72
SPMS.Application/DTOs/Stats/DashboardResponseDto.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Stats;
|
||||||
|
|
||||||
|
public class DashboardResponseDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("kpi")]
|
||||||
|
public DashboardKpiDto Kpi { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("daily")]
|
||||||
|
public List<DailyStatItemDto> Daily { get; set; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("hourly")]
|
||||||
|
public List<HourlyStatItemDto> Hourly { get; set; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("platform_share")]
|
||||||
|
public List<PlatformStatDto> PlatformShare { get; set; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("top_messages")]
|
||||||
|
public List<TopMessageDto> TopMessages { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DashboardKpiDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("total_devices")]
|
||||||
|
public int TotalDevices { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("active_devices")]
|
||||||
|
public int ActiveDevices { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_messages")]
|
||||||
|
public int TotalMessages { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_send")]
|
||||||
|
public long TotalSend { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_success")]
|
||||||
|
public long TotalSuccess { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_open")]
|
||||||
|
public long TotalOpen { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("avg_ctr")]
|
||||||
|
public double AvgCtr { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("active_service_count")]
|
||||||
|
public int ActiveServiceCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("today")]
|
||||||
|
public PeriodStatDto Today { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("this_month")]
|
||||||
|
public PeriodStatDto ThisMonth { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TopMessageDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("message_code")]
|
||||||
|
public string MessageCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("service_name")]
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("total_send_count")]
|
||||||
|
public int TotalSendCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("success_count")]
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,5 @@ public interface IStatsService
|
||||||
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request);
|
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request);
|
||||||
Task<byte[]> ExportReportAsync(long? serviceId, StatsExportRequestDto request);
|
Task<byte[]> ExportReportAsync(long? serviceId, StatsExportRequestDto request);
|
||||||
Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request);
|
Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request);
|
||||||
|
Task<DashboardResponseDto> GetDashboardAsync(long? serviceId, DashboardRequestDto request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,20 @@ public class StatsService : IStatsService
|
||||||
private readonly IPushSendLogRepository _pushSendLogRepository;
|
private readonly IPushSendLogRepository _pushSendLogRepository;
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IMessageRepository _messageRepository;
|
private readonly IMessageRepository _messageRepository;
|
||||||
|
private readonly IServiceRepository _serviceRepository;
|
||||||
|
|
||||||
public StatsService(
|
public StatsService(
|
||||||
IDailyStatRepository dailyStatRepository,
|
IDailyStatRepository dailyStatRepository,
|
||||||
IPushSendLogRepository pushSendLogRepository,
|
IPushSendLogRepository pushSendLogRepository,
|
||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
IMessageRepository messageRepository)
|
IMessageRepository messageRepository,
|
||||||
|
IServiceRepository serviceRepository)
|
||||||
{
|
{
|
||||||
_dailyStatRepository = dailyStatRepository;
|
_dailyStatRepository = dailyStatRepository;
|
||||||
_pushSendLogRepository = pushSendLogRepository;
|
_pushSendLogRepository = pushSendLogRepository;
|
||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
_messageRepository = messageRepository;
|
_messageRepository = messageRepository;
|
||||||
|
_serviceRepository = serviceRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DailyStatResponseDto> GetDailyAsync(long? serviceId, DailyStatRequestDto request)
|
public async Task<DailyStatResponseDto> GetDailyAsync(long? serviceId, DailyStatRequestDto request)
|
||||||
|
|
@ -420,6 +423,45 @@ public class StatsService : IStatsService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DashboardResponseDto> GetDashboardAsync(long? serviceId, DashboardRequestDto request)
|
||||||
|
{
|
||||||
|
// EF Core DbContext는 thread-safe하지 않으므로 순차 실행
|
||||||
|
var summary = await GetSummaryAsync(serviceId);
|
||||||
|
var daily = await GetDailyAsync(serviceId, new DailyStatRequestDto { StartDate = request.StartDate, EndDate = request.EndDate });
|
||||||
|
var hourly = await GetHourlyAsync(serviceId, new HourlyStatRequestDto { StartDate = request.StartDate, EndDate = request.EndDate });
|
||||||
|
var device = await GetDeviceStatAsync(serviceId);
|
||||||
|
var activeServiceCount = await _serviceRepository.CountAsync(s => s.Status == ServiceStatus.Active && !s.IsDeleted);
|
||||||
|
var topMessages = await _messageRepository.GetTopBySendCountAsync(serviceId, 5);
|
||||||
|
|
||||||
|
return new DashboardResponseDto
|
||||||
|
{
|
||||||
|
Kpi = new DashboardKpiDto
|
||||||
|
{
|
||||||
|
TotalDevices = summary.TotalDevices,
|
||||||
|
ActiveDevices = summary.ActiveDevices,
|
||||||
|
TotalMessages = summary.TotalMessages,
|
||||||
|
TotalSend = summary.TotalSend,
|
||||||
|
TotalSuccess = summary.TotalSuccess,
|
||||||
|
TotalOpen = summary.TotalOpen,
|
||||||
|
AvgCtr = summary.AvgCtr,
|
||||||
|
ActiveServiceCount = activeServiceCount,
|
||||||
|
Today = summary.Today,
|
||||||
|
ThisMonth = summary.ThisMonth
|
||||||
|
},
|
||||||
|
Daily = daily.Items,
|
||||||
|
Hourly = hourly.Items,
|
||||||
|
PlatformShare = device.ByPlatform,
|
||||||
|
TopMessages = topMessages.Select(m => new TopMessageDto
|
||||||
|
{
|
||||||
|
MessageCode = m.MessageCode,
|
||||||
|
Title = m.Title,
|
||||||
|
ServiceName = m.ServiceName,
|
||||||
|
TotalSendCount = m.TotalSendCount,
|
||||||
|
SuccessCount = m.SuccessCount
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetFailReasonDescription(string failReason)
|
private static string GetFailReasonDescription(string failReason)
|
||||||
{
|
{
|
||||||
return failReason switch
|
return failReason switch
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public interface IMessageRepository : IRepository<Message>
|
||||||
string? keyword = null, bool? isActive = null, string? sendStatus = null);
|
string? keyword = null, bool? isActive = null, string? sendStatus = null);
|
||||||
Task<Message?> GetByMessageCodeWithDetailsAsync(string messageCode, long serviceId);
|
Task<Message?> GetByMessageCodeWithDetailsAsync(string messageCode, long serviceId);
|
||||||
Task<(int TotalSendCount, int SuccessCount)> GetSendStatsAsync(long messageId);
|
Task<(int TotalSendCount, int SuccessCount)> GetSendStatsAsync(long messageId);
|
||||||
|
Task<IReadOnlyList<MessageListProjection>> GetTopBySendCountAsync(long? serviceId, int count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MessageListProjection
|
public class MessageListProjection
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,29 @@ public class MessageRepository : Repository<Message>, IMessageRepository
|
||||||
return stats != null ? (stats.Total, stats.Success) : (0, 0);
|
return stats != null ? (stats.Total, stats.Success) : (0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<MessageListProjection>> GetTopBySendCountAsync(long? serviceId, int count)
|
||||||
|
{
|
||||||
|
var query = _dbSet.Where(m => !m.IsDeleted);
|
||||||
|
if (serviceId.HasValue)
|
||||||
|
query = query.Where(m => m.ServiceId == serviceId.Value);
|
||||||
|
|
||||||
|
return await 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)
|
||||||
|
})
|
||||||
|
.Where(p => p.TotalSendCount > 0)
|
||||||
|
.OrderByDescending(p => p.TotalSendCount)
|
||||||
|
.Take(count)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync(
|
public async Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync(
|
||||||
long? serviceId, int page, int size,
|
long? serviceId, int page, int size,
|
||||||
string? keyword = null, bool? isActive = null, string? sendStatus = null)
|
string? keyword = null, bool? isActive = null, string? sendStatus = null)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user