feat: 실패원인 순위 API 구현 (#142) #143
|
|
@ -73,6 +73,15 @@ public class StatsController : ControllerBase
|
|||
return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
|
||||
}
|
||||
|
||||
[HttpPost("failure")]
|
||||
[SwaggerOperation(Summary = "실패원인 통계 조회", Description = "실패 원인별 집계를 상위 N개로 조회합니다.")]
|
||||
public async Task<IActionResult> GetFailureStatAsync([FromBody] FailureStatRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var result = await _statsService.GetFailureStatAsync(serviceId, request);
|
||||
return Ok(ApiResponse<FailureStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
||||
[HttpPost("send-log")]
|
||||
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")]
|
||||
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
|
||||
|
|
|
|||
19
SPMS.Application/DTOs/Stats/FailureStatRequestDto.cs
Normal file
19
SPMS.Application/DTOs/Stats/FailureStatRequestDto.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Stats;
|
||||
|
||||
public class FailureStatRequestDto
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("start_date")]
|
||||
public string StartDate { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[JsonPropertyName("end_date")]
|
||||
public string EndDate { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
[Range(1, 20)]
|
||||
public int Limit { get; set; } = 5;
|
||||
}
|
||||
39
SPMS.Application/DTOs/Stats/FailureStatResponseDto.cs
Normal file
39
SPMS.Application/DTOs/Stats/FailureStatResponseDto.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Stats;
|
||||
|
||||
public class FailureStatResponseDto
|
||||
{
|
||||
[JsonPropertyName("total_fail")]
|
||||
public int TotalFail { get; set; }
|
||||
|
||||
[JsonPropertyName("period")]
|
||||
public FailureStatPeriodDto Period { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("items")]
|
||||
public List<FailureStatItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class FailureStatPeriodDto
|
||||
{
|
||||
[JsonPropertyName("start_date")]
|
||||
public string StartDate { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("end_date")]
|
||||
public string EndDate { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class FailureStatItemDto
|
||||
{
|
||||
[JsonPropertyName("fail_reason")]
|
||||
public string FailReason { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("count")]
|
||||
public int Count { get; set; }
|
||||
|
||||
[JsonPropertyName("ratio")]
|
||||
public double Ratio { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -11,4 +11,5 @@ public interface IStatsService
|
|||
Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId);
|
||||
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request);
|
||||
Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request);
|
||||
Task<FailureStatResponseDto> GetFailureStatAsync(long serviceId, FailureStatRequestDto request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,6 +373,52 @@ public class StatsService : IStatsService
|
|||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public async Task<FailureStatResponseDto> GetFailureStatAsync(long serviceId, FailureStatRequestDto request)
|
||||
{
|
||||
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
|
||||
|
||||
var startDateTime = startDate.ToDateTime(TimeOnly.MinValue);
|
||||
var endDateTime = endDate.ToDateTime(TimeOnly.MinValue).AddDays(1);
|
||||
|
||||
var failureStats = await _pushSendLogRepository.GetFailureStatsAsync(serviceId, startDateTime, endDateTime, request.Limit);
|
||||
|
||||
var totalFail = failureStats.Sum(f => f.Count);
|
||||
|
||||
var items = failureStats.Select(f => new FailureStatItemDto
|
||||
{
|
||||
FailReason = f.FailReason,
|
||||
Count = f.Count,
|
||||
Ratio = totalFail > 0 ? Math.Round((double)f.Count / totalFail * 100, 2) : 0,
|
||||
Description = GetFailReasonDescription(f.FailReason)
|
||||
}).ToList();
|
||||
|
||||
return new FailureStatResponseDto
|
||||
{
|
||||
TotalFail = totalFail,
|
||||
Period = new FailureStatPeriodDto
|
||||
{
|
||||
StartDate = request.StartDate,
|
||||
EndDate = request.EndDate
|
||||
},
|
||||
Items = items
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetFailReasonDescription(string failReason)
|
||||
{
|
||||
return failReason switch
|
||||
{
|
||||
"InvalidToken" => "만료/잘못된 디바이스 토큰",
|
||||
"NotRegistered" => "FCM/APNs에 미등록",
|
||||
"MessageTooBig" => "페이로드 크기 초과 (4KB)",
|
||||
"RateLimit" => "FCM/APNs 발송 제한 초과",
|
||||
"ServerError" => "FCM/APNs 서버 오류",
|
||||
"NetworkError" => "네트워크 연결 실패",
|
||||
"Unknown" => "분류 불가",
|
||||
_ => failReason
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalcCtr(int openCount, int successCount)
|
||||
{
|
||||
return successCount > 0 ? Math.Round((double)openCount / successCount * 100, 2) : 0;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public interface IPushSendLogRepository : IRepository<PushSendLog>
|
|||
long serviceId, DateTime startDate, DateTime endDate,
|
||||
long? messageId = null, long? deviceId = null,
|
||||
PushResult? status = null, int maxCount = 100000);
|
||||
Task<List<FailureStatRaw>> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit);
|
||||
}
|
||||
|
||||
public class HourlyStatRaw
|
||||
|
|
@ -46,3 +47,9 @@ public class MessageDailyStatRaw
|
|||
public int SendCount { get; set; }
|
||||
public int SuccessCount { get; set; }
|
||||
}
|
||||
|
||||
public class FailureStatRaw
|
||||
{
|
||||
public string FailReason { get; set; } = string.Empty;
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,4 +156,23 @@ public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogReposi
|
|||
.OrderByDescending(d => d.StatDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<FailureStatRaw>> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit)
|
||||
{
|
||||
return await _dbSet
|
||||
.Where(l => l.ServiceId == serviceId
|
||||
&& l.Status == PushResult.Failed
|
||||
&& l.SentAt >= startDate
|
||||
&& l.SentAt < endDate
|
||||
&& l.FailReason != null)
|
||||
.GroupBy(l => l.FailReason!)
|
||||
.Select(g => new FailureStatRaw
|
||||
{
|
||||
FailReason = g.Key,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(f => f.Count)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
TASKS.md
10
TASKS.md
|
|
@ -18,14 +18,14 @@
|
|||
|--------|:------:|:---------:|:------:|-------|
|
||||
| **Public** | 10 | 10 | 0 | Phase 2-2 ✅ |
|
||||
| **Auth** | 6 | 6 | 0 | Phase 2-1 ✅ |
|
||||
| **Account** | 5 | 5 | 0 | Phase 2-1 ✅ |
|
||||
| **Account** | 9 | 9 | 0 | Phase 2-1 + 3-2 ✅ |
|
||||
| **Service** | 13 | 13 | 0 | Phase 2-1 ✅ |
|
||||
| **Device** | 7 | 7 | 0 | Phase 2-2 ✅ |
|
||||
| **Message** | 5 | 1 | 4 | Phase 3 ← 다음 |
|
||||
| **Push** | 8 | 5 | 3 | Phase 3 |
|
||||
| **Stats** | 5 | 0 | 5 | Phase 3-2 |
|
||||
| **File** | 6 | 6 | 0 | Phase 2-2 ✅ |
|
||||
| **총계** | **65** | **53** | **12** | - |
|
||||
| **총계** | **65** | **57** | **8** | - |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -1649,9 +1649,9 @@ Milestone: Phase 3: 메시지 & Push Core
|
|||
| 3 | [Feature] 플랫폼별 비중 조회 API | Feature | Medium | DSH-03 | ✅ |
|
||||
| 4 | [Feature] 메시지별 전환율 조회 API | Feature | Medium | DSH-04 | ✅ |
|
||||
| 5 | [Feature] 발송 이력 조회 API | Feature | High | DDN-01 | ✅ |
|
||||
| 6 | [Feature] 발송 상세 로그 조회 API | Feature | High | DDL-02 | ⬜ |
|
||||
| 7 | [Feature] 통계 리포트 다운로드 API | Feature | Medium | EXP-01 | ⬜ |
|
||||
| 8 | [Feature] 상세 로그 다운로드 API | Feature | Medium | EXP-02 | ⬜ |
|
||||
| 6 | [Feature] 발송 상세 로그 조회 API | Feature | High | DDL-02 | ✅ |
|
||||
| 7 | [Feature] 통계 리포트 다운로드 API | Feature | Medium | EXP-01 | ✅ |
|
||||
| 8 | [Feature] 상세 로그 다운로드 API | Feature | Medium | EXP-02 | ✅ |
|
||||
| 9 | [Feature] 실패원인 순위 API | Feature | Medium | ANA-01 | ⬜ |
|
||||
| 10 | [Feature] 웹훅 설정 API | Feature | High | WHK-01 | ⬜ |
|
||||
| 11 | [Feature] **웹훅 발송 서비스** | Feature | High | WHK-02 | ⬜ |
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user