feat: 통계 리포트 다운로드 API 구현 (#138)
- POST /v1/in/stats/export (EXP-01) - 일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 3시트로 생성 - ClosedXML 패키지 추가 Closes #138
This commit is contained in:
parent
ce266956c7
commit
d3bd4356a8
|
|
@ -63,6 +63,16 @@ public class StatsController : ControllerBase
|
|||
return Ok(ApiResponse<DeviceStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
||||
[HttpPost("export")]
|
||||
[SwaggerOperation(Summary = "통계 리포트 다운로드", Description = "일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 파일로 다운로드합니다.")]
|
||||
public async Task<IActionResult> ExportReportAsync([FromBody] StatsExportRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var fileBytes = await _statsService.ExportReportAsync(serviceId, request);
|
||||
var fileName = $"stats_report_{request.StartDate}_{request.EndDate}.xlsx";
|
||||
return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
|
||||
}
|
||||
|
||||
[HttpPost("send-log")]
|
||||
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")]
|
||||
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
|
||||
|
|
|
|||
15
SPMS.Application/DTOs/Stats/StatsExportRequestDto.cs
Normal file
15
SPMS.Application/DTOs/Stats/StatsExportRequestDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Stats;
|
||||
|
||||
public class StatsExportRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "start_date는 필수입니다.")]
|
||||
[JsonPropertyName("start_date")]
|
||||
public string StartDate { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "end_date는 필수입니다.")]
|
||||
[JsonPropertyName("end_date")]
|
||||
public string EndDate { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -10,4 +10,5 @@ public interface IStatsService
|
|||
Task<HourlyStatResponseDto> GetHourlyAsync(long serviceId, HourlyStatRequestDto request);
|
||||
Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId);
|
||||
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request);
|
||||
Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using ClosedXML.Excel;
|
||||
using SPMS.Application.DTOs.Stats;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
|
|
@ -290,6 +291,88 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request)
|
||||
{
|
||||
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
|
||||
// Sheet 1: 일별 통계
|
||||
var dailyStats = await _dailyStatRepository.GetByDateRangeAsync(serviceId, startDate, endDate);
|
||||
var ws1 = workbook.Worksheets.Add("일별 통계");
|
||||
ws1.Cell(1, 1).Value = "날짜";
|
||||
ws1.Cell(1, 2).Value = "발송";
|
||||
ws1.Cell(1, 3).Value = "성공";
|
||||
ws1.Cell(1, 4).Value = "실패";
|
||||
ws1.Cell(1, 5).Value = "열람";
|
||||
ws1.Cell(1, 6).Value = "CTR(%)";
|
||||
ws1.Row(1).Style.Font.Bold = true;
|
||||
|
||||
var row = 2;
|
||||
foreach (var s in dailyStats.OrderBy(s => s.StatDate))
|
||||
{
|
||||
ws1.Cell(row, 1).Value = s.StatDate.ToString("yyyy-MM-dd");
|
||||
ws1.Cell(row, 2).Value = s.SentCnt;
|
||||
ws1.Cell(row, 3).Value = s.SuccessCnt;
|
||||
ws1.Cell(row, 4).Value = s.FailCnt;
|
||||
ws1.Cell(row, 5).Value = s.OpenCnt;
|
||||
ws1.Cell(row, 6).Value = CalcCtr(s.OpenCnt, s.SuccessCnt);
|
||||
row++;
|
||||
}
|
||||
ws1.Columns().AdjustToContents();
|
||||
|
||||
// Sheet 2: 시간대별 통계
|
||||
var startDateTime = startDate.ToDateTime(TimeOnly.MinValue);
|
||||
var endDateTime = endDate.ToDateTime(TimeOnly.MinValue).AddDays(1);
|
||||
var hourlyStats = await _pushSendLogRepository.GetHourlyStatsAsync(serviceId, startDateTime, endDateTime);
|
||||
|
||||
var ws2 = workbook.Worksheets.Add("시간대별 통계");
|
||||
ws2.Cell(1, 1).Value = "시간";
|
||||
ws2.Cell(1, 2).Value = "발송";
|
||||
ws2.Cell(1, 3).Value = "성공";
|
||||
ws2.Cell(1, 4).Value = "실패";
|
||||
ws2.Row(1).Style.Font.Bold = true;
|
||||
|
||||
for (var hour = 0; hour < 24; hour++)
|
||||
{
|
||||
var stat = hourlyStats.FirstOrDefault(h => h.Hour == hour);
|
||||
ws2.Cell(hour + 2, 1).Value = $"{hour:D2}:00";
|
||||
ws2.Cell(hour + 2, 2).Value = stat?.SendCount ?? 0;
|
||||
ws2.Cell(hour + 2, 3).Value = stat?.SuccessCount ?? 0;
|
||||
ws2.Cell(hour + 2, 4).Value = stat?.FailCount ?? 0;
|
||||
}
|
||||
ws2.Columns().AdjustToContents();
|
||||
|
||||
// Sheet 3: 플랫폼별 통계
|
||||
var devices = await _deviceRepository.FindAsync(d => d.ServiceId == serviceId && d.IsActive);
|
||||
var total = devices.Count;
|
||||
|
||||
var ws3 = workbook.Worksheets.Add("플랫폼별 통계");
|
||||
ws3.Cell(1, 1).Value = "플랫폼";
|
||||
ws3.Cell(1, 2).Value = "수량";
|
||||
ws3.Cell(1, 3).Value = "비율(%)";
|
||||
ws3.Row(1).Style.Font.Bold = true;
|
||||
|
||||
var platformGroups = devices
|
||||
.GroupBy(d => d.Platform)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ToList();
|
||||
|
||||
row = 2;
|
||||
foreach (var g in platformGroups)
|
||||
{
|
||||
ws3.Cell(row, 1).Value = g.Key.ToString();
|
||||
ws3.Cell(row, 2).Value = g.Count();
|
||||
ws3.Cell(row, 3).Value = total > 0 ? Math.Round((double)g.Count() / total * 100, 2) : 0;
|
||||
row++;
|
||||
}
|
||||
ws3.Columns().AdjustToContents();
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static double CalcCtr(int openCount, int successCount)
|
||||
{
|
||||
return successCount > 0 ? Math.Round((double)openCount / successCount * 100, 2) : 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user