improvement: 대시보드 통합 API 추가 (#231) #232

Merged
seonkyu.kim merged 1 commits from improvement/#231-dashboard-api into develop 2026-02-25 07:12:26 +00:00
7 changed files with 161 additions and 1 deletions

View File

@ -82,6 +82,15 @@ public class StatsController : ControllerBase
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")]
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")]
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)

View 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;
}

View 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; }
}

View File

@ -12,4 +12,5 @@ public interface IStatsService
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request);
Task<byte[]> ExportReportAsync(long? serviceId, StatsExportRequestDto request);
Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request);
Task<DashboardResponseDto> GetDashboardAsync(long? serviceId, DashboardRequestDto request);
}

View File

@ -16,17 +16,20 @@ public class StatsService : IStatsService
private readonly IPushSendLogRepository _pushSendLogRepository;
private readonly IDeviceRepository _deviceRepository;
private readonly IMessageRepository _messageRepository;
private readonly IServiceRepository _serviceRepository;
public StatsService(
IDailyStatRepository dailyStatRepository,
IPushSendLogRepository pushSendLogRepository,
IDeviceRepository deviceRepository,
IMessageRepository messageRepository)
IMessageRepository messageRepository,
IServiceRepository serviceRepository)
{
_dailyStatRepository = dailyStatRepository;
_pushSendLogRepository = pushSendLogRepository;
_deviceRepository = deviceRepository;
_messageRepository = messageRepository;
_serviceRepository = serviceRepository;
}
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)
{
return failReason switch

View File

@ -16,6 +16,7 @@ public interface IMessageRepository : IRepository<Message>
string? keyword = null, bool? isActive = null, string? sendStatus = null);
Task<Message?> GetByMessageCodeWithDetailsAsync(string messageCode, long serviceId);
Task<(int TotalSendCount, int SuccessCount)> GetSendStatsAsync(long messageId);
Task<IReadOnlyList<MessageListProjection>> GetTopBySendCountAsync(long? serviceId, int count);
}
public class MessageListProjection

View File

@ -77,6 +77,29 @@ public class MessageRepository : Repository<Message>, IMessageRepository
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(
long? serviceId, int page, int size,
string? keyword = null, bool? isActive = null, string? sendStatus = null)