feat: 통계 리포트 다운로드 API 구현 (#138)
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/139
This commit is contained in:
commit
042e6e1dd6
|
|
@ -63,6 +63,16 @@ public class StatsController : ControllerBase
|
||||||
return Ok(ApiResponse<DeviceStatResponseDto>.Success(result, "조회 성공"));
|
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")]
|
[HttpPost("send-log")]
|
||||||
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")]
|
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")]
|
||||||
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
|
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<HourlyStatResponseDto> GetHourlyAsync(long serviceId, HourlyStatRequestDto request);
|
||||||
Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId);
|
Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId);
|
||||||
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request);
|
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request);
|
||||||
|
Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<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.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using ClosedXML.Excel;
|
||||||
using SPMS.Application.DTOs.Stats;
|
using SPMS.Application.DTOs.Stats;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
using SPMS.Domain.Common;
|
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)
|
private static double CalcCtr(int openCount, int successCount)
|
||||||
{
|
{
|
||||||
return successCount > 0 ? Math.Round((double)openCount / successCount * 100, 2) : 0;
|
return successCount > 0 ? Math.Round((double)openCount / successCount * 100, 2) : 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user