improvement: 서비스 스코프 정책 고정 (#199) #200
|
|
@ -78,7 +78,7 @@ public class DeviceController : ControllerBase
|
|||
[SwaggerOperation(Summary = "디바이스 목록", Description = "대시보드에서 디바이스 목록을 조회합니다. JWT 인증 필요.")]
|
||||
public async Task<IActionResult> GetListAsync([FromBody] DeviceListRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _deviceService.GetListAsync(serviceId, request);
|
||||
return Ok(ApiResponse<DeviceListResponseDto>.Success(result));
|
||||
}
|
||||
|
|
@ -90,4 +90,11 @@ public class DeviceController : ControllerBase
|
|||
|
||||
throw new Domain.Exceptions.SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
|
||||
}
|
||||
|
||||
private long? GetOptionalServiceId()
|
||||
{
|
||||
if (HttpContext.Items.TryGetValue("ServiceId", out var obj) && obj is long id)
|
||||
return id;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "일별 통계 조회", Description = "기간별 일별 발송/성공/실패/열람 통계를 조회합니다.")]
|
||||
public async Task<IActionResult> GetDailyAsync([FromBody] DailyStatRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetDailyAsync(serviceId, request);
|
||||
return Ok(ApiResponse<DailyStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "요약 통계 조회", Description = "대시보드 요약 통계를 조회합니다.")]
|
||||
public async Task<IActionResult> GetSummaryAsync()
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetSummaryAsync(serviceId);
|
||||
return Ok(ApiResponse<SummaryStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "메시지별 통계 조회", Description = "특정 메시지의 발송 통계를 조회합니다.")]
|
||||
public async Task<IActionResult> GetMessageStatAsync([FromBody] MessageStatRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetMessageStatAsync(serviceId, request);
|
||||
return Ok(ApiResponse<MessageStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "시간대별 통계 조회", Description = "시간대별 발송 추이를 조회합니다.")]
|
||||
public async Task<IActionResult> GetHourlyAsync([FromBody] HourlyStatRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetHourlyAsync(serviceId, request);
|
||||
return Ok(ApiResponse<HourlyStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "디바이스 통계 조회", Description = "플랫폼/모델별 디바이스 분포를 조회합니다.")]
|
||||
public async Task<IActionResult> GetDeviceStatAsync()
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetDeviceStatAsync(serviceId);
|
||||
return Ok(ApiResponse<DeviceStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "통계 리포트 다운로드", Description = "일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 파일로 다운로드합니다.")]
|
||||
public async Task<IActionResult> ExportReportAsync([FromBody] StatsExportRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
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);
|
||||
|
|
@ -77,7 +77,7 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "실패원인 통계 조회", Description = "실패 원인별 집계를 상위 N개로 조회합니다.")]
|
||||
public async Task<IActionResult> GetFailureStatAsync([FromBody] FailureStatRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetFailureStatAsync(serviceId, request);
|
||||
return Ok(ApiResponse<FailureStatResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
|
@ -86,16 +86,15 @@ public class StatsController : ControllerBase
|
|||
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")]
|
||||
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var serviceId = GetOptionalServiceId();
|
||||
var result = await _statsService.GetSendLogDetailAsync(serviceId, request);
|
||||
return Ok(ApiResponse<SendLogDetailResponseDto>.Success(result, "조회 성공"));
|
||||
}
|
||||
|
||||
private long GetServiceId()
|
||||
private long? GetOptionalServiceId()
|
||||
{
|
||||
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
|
||||
return serviceId;
|
||||
|
||||
throw new Domain.Exceptions.SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
|
||||
if (HttpContext.Items.TryGetValue("ServiceId", out var obj) && obj is long id)
|
||||
return id;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,26 @@ public class SpmsHeaderOperationFilter : IOperationFilter
|
|||
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
// v1/in/* 중 X-Service-Code가 필요한 경로만 (device, message, push, stats, file)
|
||||
if (relativePath.StartsWith("v1/in/device") ||
|
||||
relativePath.StartsWith("v1/in/message") ||
|
||||
// v1/in/* 중 X-Service-Code 대상 경로 판별
|
||||
var isStatsOrDeviceList = relativePath.StartsWith("v1/in/stats") ||
|
||||
relativePath == "v1/in/device/list";
|
||||
var isRequired = relativePath.StartsWith("v1/in/message") ||
|
||||
relativePath.StartsWith("v1/in/push") ||
|
||||
relativePath.StartsWith("v1/in/stats") ||
|
||||
relativePath.StartsWith("v1/in/file"))
|
||||
relativePath.StartsWith("v1/in/file");
|
||||
var isDeviceNonList = relativePath.StartsWith("v1/in/device") && !isStatsOrDeviceList;
|
||||
|
||||
if (isStatsOrDeviceList)
|
||||
{
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "X-Service-Code",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "서비스 식별 코드 (관리자: 미지정 시 전체 서비스 조회)",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
}
|
||||
else if (isRequired || isDeviceNonList)
|
||||
{
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,20 +12,52 @@ public class ServiceCodeMiddleware
|
|||
|
||||
public async Task InvokeAsync(HttpContext context, IServiceRepository serviceRepository)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/v1/out") ||
|
||||
context.Request.Path.StartsWithSegments("/v1/in/auth") ||
|
||||
context.Request.Path.StartsWithSegments("/v1/in/account") ||
|
||||
context.Request.Path.StartsWithSegments("/v1/in/public") ||
|
||||
context.Request.Path.StartsWithSegments("/v1/in/service") ||
|
||||
(context.Request.Path.StartsWithSegments("/v1/in/device") &&
|
||||
!context.Request.Path.StartsWithSegments("/v1/in/device/list")) ||
|
||||
context.Request.Path.StartsWithSegments("/swagger") ||
|
||||
context.Request.Path.StartsWithSegments("/health"))
|
||||
var path = context.Request.Path;
|
||||
|
||||
// === SKIP: X-Service-Code 불필요 ===
|
||||
if (path.StartsWithSegments("/v1/out") ||
|
||||
path.StartsWithSegments("/v1/in/auth") ||
|
||||
path.StartsWithSegments("/v1/in/account") ||
|
||||
path.StartsWithSegments("/v1/in/public") ||
|
||||
path.StartsWithSegments("/v1/in/service") ||
|
||||
(path.StartsWithSegments("/v1/in/device") &&
|
||||
!path.StartsWithSegments("/v1/in/device/list")) ||
|
||||
path.StartsWithSegments("/swagger") ||
|
||||
path.StartsWithSegments("/health"))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// === OPTIONAL_FOR_ADMIN: 관리자는 X-Service-Code 선택 ===
|
||||
if (path.StartsWithSegments("/v1/in/stats") ||
|
||||
path.StartsWithSegments("/v1/in/device/list"))
|
||||
{
|
||||
if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) &&
|
||||
!string.IsNullOrWhiteSpace(optionalCode))
|
||||
{
|
||||
// 헤더가 있으면 기존 검증 수행
|
||||
await ValidateAndSetService(context, serviceRepository, optionalCode!);
|
||||
return;
|
||||
}
|
||||
|
||||
// 헤더 없음 — 인증된 사용자만 전체 서비스 모드 허용
|
||||
if (context.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
// ServiceId 미설정 = 전체 서비스 모드
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 비인증 요청 → 에러
|
||||
context.Response.StatusCode = 400;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(
|
||||
ApiResponse.Fail(ErrorCodes.ServiceScopeRequired, "X-Service-Code 헤더가 필요합니다."));
|
||||
return;
|
||||
}
|
||||
|
||||
// === REQUIRED: X-Service-Code 필수 ===
|
||||
if (!context.Request.Headers.TryGetValue("X-Service-Code", out var serviceCode) ||
|
||||
string.IsNullOrWhiteSpace(serviceCode))
|
||||
{
|
||||
|
|
@ -36,7 +68,12 @@ public class ServiceCodeMiddleware
|
|||
return;
|
||||
}
|
||||
|
||||
var service = await serviceRepository.GetByServiceCodeAsync(serviceCode!);
|
||||
await ValidateAndSetService(context, serviceRepository, serviceCode!);
|
||||
}
|
||||
|
||||
private async Task ValidateAndSetService(HttpContext context, IServiceRepository serviceRepository, string serviceCode)
|
||||
{
|
||||
var service = await serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||
if (service == null)
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public interface IDeviceService
|
|||
Task<DeviceInfoResponseDto> GetInfoAsync(long serviceId, DeviceInfoRequestDto request);
|
||||
Task UpdateAsync(long serviceId, DeviceUpdateRequestDto request);
|
||||
Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request);
|
||||
Task<DeviceListResponseDto> GetListAsync(long serviceId, DeviceListRequestDto request);
|
||||
Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request);
|
||||
Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request);
|
||||
Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ namespace SPMS.Application.Interfaces;
|
|||
|
||||
public interface IStatsService
|
||||
{
|
||||
Task<DailyStatResponseDto> GetDailyAsync(long serviceId, DailyStatRequestDto request);
|
||||
Task<SummaryStatResponseDto> GetSummaryAsync(long serviceId);
|
||||
Task<MessageStatResponseDto> GetMessageStatAsync(long serviceId, MessageStatRequestDto request);
|
||||
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);
|
||||
Task<FailureStatResponseDto> GetFailureStatAsync(long serviceId, FailureStatRequestDto request);
|
||||
Task<DailyStatResponseDto> GetDailyAsync(long? serviceId, DailyStatRequestDto request);
|
||||
Task<SummaryStatResponseDto> GetSummaryAsync(long? serviceId);
|
||||
Task<MessageStatResponseDto> GetMessageStatAsync(long? serviceId, MessageStatRequestDto request);
|
||||
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);
|
||||
Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ public class DeviceService : IDeviceService
|
|||
await _tokenCache.InvalidateAsync(serviceId, device.Id);
|
||||
}
|
||||
|
||||
public async Task<DeviceListResponseDto> GetListAsync(long serviceId, DeviceListRequestDto request)
|
||||
public async Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request)
|
||||
{
|
||||
Platform? platform = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.Platform))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class StatsService : IStatsService
|
|||
_messageRepository = messageRepository;
|
||||
}
|
||||
|
||||
public async Task<DailyStatResponseDto> GetDailyAsync(long serviceId, DailyStatRequestDto request)
|
||||
public async Task<DailyStatResponseDto> GetDailyAsync(long? serviceId, DailyStatRequestDto request)
|
||||
{
|
||||
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
|
||||
|
||||
|
|
@ -64,13 +64,21 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<SummaryStatResponseDto> GetSummaryAsync(long serviceId)
|
||||
public async Task<SummaryStatResponseDto> GetSummaryAsync(long? serviceId)
|
||||
{
|
||||
var totalDevices = await _deviceRepository.CountAsync(d => d.ServiceId == serviceId);
|
||||
var activeDevices = await _deviceRepository.CountAsync(d => d.ServiceId == serviceId && d.IsActive);
|
||||
var totalMessages = await _messageRepository.CountAsync(m => m.ServiceId == serviceId && !m.IsDeleted);
|
||||
var totalDevices = serviceId.HasValue
|
||||
? await _deviceRepository.CountAsync(d => d.ServiceId == serviceId.Value)
|
||||
: await _deviceRepository.CountAsync(d => true);
|
||||
var activeDevices = serviceId.HasValue
|
||||
? await _deviceRepository.CountAsync(d => d.ServiceId == serviceId.Value && d.IsActive)
|
||||
: await _deviceRepository.CountAsync(d => d.IsActive);
|
||||
var totalMessages = serviceId.HasValue
|
||||
? await _messageRepository.CountAsync(m => m.ServiceId == serviceId.Value && !m.IsDeleted)
|
||||
: await _messageRepository.CountAsync(m => !m.IsDeleted);
|
||||
|
||||
var allStats = await _dailyStatRepository.FindAsync(s => s.ServiceId == serviceId);
|
||||
var allStats = serviceId.HasValue
|
||||
? await _dailyStatRepository.FindAsync(s => s.ServiceId == serviceId.Value)
|
||||
: await _dailyStatRepository.FindAsync(s => true);
|
||||
var totalSend = allStats.Sum(s => (long)s.SentCnt);
|
||||
var totalSuccess = allStats.Sum(s => (long)s.SuccessCnt);
|
||||
var totalOpen = allStats.Sum(s => (long)s.OpenCnt);
|
||||
|
|
@ -107,20 +115,22 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<MessageStatResponseDto> GetMessageStatAsync(long serviceId, MessageStatRequestDto request)
|
||||
public async Task<MessageStatResponseDto> GetMessageStatAsync(long? serviceId, MessageStatRequestDto request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.MessageCode))
|
||||
throw new SpmsException(ErrorCodes.BadRequest, "message_code는 필수입니다.", 400);
|
||||
|
||||
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
||||
var message = serviceId.HasValue
|
||||
? await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId.Value)
|
||||
: await _messageRepository.GetByMessageCodeAsync(request.MessageCode);
|
||||
if (message == null)
|
||||
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
||||
|
||||
DateTime? startDate = ParseOptionalDate(request.StartDate);
|
||||
DateTime? endDate = ParseOptionalDate(request.EndDate);
|
||||
|
||||
var stats = await _pushSendLogRepository.GetMessageStatsAsync(serviceId, message.Id, startDate, endDate);
|
||||
var dailyStats = await _pushSendLogRepository.GetMessageDailyStatsAsync(serviceId, message.Id, startDate, endDate);
|
||||
var stats = await _pushSendLogRepository.GetMessageStatsAsync(message.ServiceId, message.Id, startDate, endDate);
|
||||
var dailyStats = await _pushSendLogRepository.GetMessageDailyStatsAsync(message.ServiceId, message.Id, startDate, endDate);
|
||||
|
||||
return new MessageStatResponseDto
|
||||
{
|
||||
|
|
@ -143,7 +153,7 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<HourlyStatResponseDto> GetHourlyAsync(long serviceId, HourlyStatRequestDto request)
|
||||
public async Task<HourlyStatResponseDto> GetHourlyAsync(long? serviceId, HourlyStatRequestDto request)
|
||||
{
|
||||
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
|
||||
|
||||
|
|
@ -178,9 +188,11 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId)
|
||||
public async Task<DeviceStatResponseDto> GetDeviceStatAsync(long? serviceId)
|
||||
{
|
||||
var devices = await _deviceRepository.FindAsync(d => d.ServiceId == serviceId && d.IsActive);
|
||||
var devices = serviceId.HasValue
|
||||
? await _deviceRepository.FindAsync(d => d.ServiceId == serviceId.Value && d.IsActive)
|
||||
: await _deviceRepository.FindAsync(d => d.IsActive);
|
||||
var total = devices.Count;
|
||||
|
||||
var byPlatform = devices
|
||||
|
|
@ -236,9 +248,11 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request)
|
||||
public async Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request)
|
||||
{
|
||||
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
||||
var message = serviceId.HasValue
|
||||
? await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId.Value)
|
||||
: await _messageRepository.GetByMessageCodeAsync(request.MessageCode);
|
||||
if (message == null)
|
||||
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
||||
|
||||
|
|
@ -266,7 +280,7 @@ public class StatsService : IStatsService
|
|||
}
|
||||
|
||||
var (items, totalCount) = await _pushSendLogRepository.GetDetailLogPagedAsync(
|
||||
serviceId, message.Id, request.Page, request.Size, status, platform);
|
||||
message.ServiceId, message.Id, request.Page, request.Size, status, platform);
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
||||
|
||||
|
|
@ -291,7 +305,7 @@ public class StatsService : IStatsService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request)
|
||||
public async Task<byte[]> ExportReportAsync(long? serviceId, StatsExportRequestDto request)
|
||||
{
|
||||
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
|
||||
|
||||
|
|
@ -344,7 +358,9 @@ public class StatsService : IStatsService
|
|||
ws2.Columns().AdjustToContents();
|
||||
|
||||
// Sheet 3: 플랫폼별 통계
|
||||
var devices = await _deviceRepository.FindAsync(d => d.ServiceId == serviceId && d.IsActive);
|
||||
var devices = serviceId.HasValue
|
||||
? await _deviceRepository.FindAsync(d => d.ServiceId == serviceId.Value && d.IsActive)
|
||||
: await _deviceRepository.FindAsync(d => d.IsActive);
|
||||
var total = devices.Count;
|
||||
|
||||
var ws3 = workbook.Worksheets.Add("플랫폼별 통계");
|
||||
|
|
@ -373,7 +389,7 @@ public class StatsService : IStatsService
|
|||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public async Task<FailureStatResponseDto> GetFailureStatAsync(long serviceId, FailureStatRequestDto request)
|
||||
public async Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request)
|
||||
{
|
||||
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public static class ErrorCodes
|
|||
// === Service (3) ===
|
||||
public const string DecryptionFailed = "131";
|
||||
public const string InvalidCredentials = "132";
|
||||
public const string ServiceScopeRequired = "133";
|
||||
|
||||
// === Device (4) ===
|
||||
public const string DeviceNotFound = "141";
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ public class SpmsException : Exception
|
|||
public static SpmsException Conflict(string message)
|
||||
=> new(ErrorCodes.Conflict, message, 409);
|
||||
|
||||
public static SpmsException Forbidden(string message)
|
||||
=> new(ErrorCodes.Forbidden, message, 403);
|
||||
|
||||
public static SpmsException LimitExceeded(string message)
|
||||
=> new(ErrorCodes.LimitExceeded, message, 429);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace SPMS.Domain.Interfaces;
|
|||
|
||||
public interface IDailyStatRepository : IRepository<DailyStat>
|
||||
{
|
||||
Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate);
|
||||
Task<DailyStat?> GetByDateAsync(long serviceId, DateOnly date);
|
||||
Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long? serviceId, DateOnly startDate, DateOnly endDate);
|
||||
Task<DailyStat?> GetByDateAsync(long? serviceId, DateOnly date);
|
||||
Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public interface IDeviceRepository : IRepository<Device>
|
|||
Task<int> GetActiveCountByServiceAsync(long serviceId);
|
||||
Task<IReadOnlyList<Device>> GetByPlatformAsync(long serviceId, Platform platform);
|
||||
Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
|
||||
long serviceId, int page, int size,
|
||||
long? serviceId, int page, int size,
|
||||
Platform? platform = null, bool? pushAgreed = null,
|
||||
bool? isActive = null, List<int>? tags = null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public interface IPushSendLogRepository : IRepository<PushSendLog>
|
|||
PushResult? status = null,
|
||||
DateTime? startDate = null, DateTime? endDate = null);
|
||||
|
||||
Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long serviceId, DateTime startDate, DateTime endDate);
|
||||
Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long? serviceId, DateTime startDate, DateTime endDate);
|
||||
Task<MessageStatRaw?> GetMessageStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate);
|
||||
Task<List<MessageDailyStatRaw>> GetMessageDailyStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate);
|
||||
Task<(IReadOnlyList<PushSendLog> Items, int TotalCount)> GetDetailLogPagedAsync(
|
||||
|
|
@ -21,7 +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);
|
||||
Task<List<FailureStatRaw>> GetFailureStatsAsync(long? serviceId, DateTime startDate, DateTime endDate, int limit);
|
||||
}
|
||||
|
||||
public class HourlyStatRaw
|
||||
|
|
|
|||
|
|
@ -8,18 +8,20 @@ public class DailyStatRepository : Repository<DailyStat>, IDailyStatRepository
|
|||
{
|
||||
public DailyStatRepository(AppDbContext context) : base(context) { }
|
||||
|
||||
public async Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate)
|
||||
public async Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long? serviceId, DateOnly startDate, DateOnly endDate)
|
||||
{
|
||||
return await _dbSet
|
||||
.Where(s => s.ServiceId == serviceId && s.StatDate >= startDate && s.StatDate <= endDate)
|
||||
.OrderByDescending(s => s.StatDate)
|
||||
.ToListAsync();
|
||||
var query = _dbSet.Where(s => s.StatDate >= startDate && s.StatDate <= endDate);
|
||||
if (serviceId.HasValue)
|
||||
query = query.Where(s => s.ServiceId == serviceId.Value);
|
||||
return await query.OrderByDescending(s => s.StatDate).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<DailyStat?> GetByDateAsync(long serviceId, DateOnly date)
|
||||
public async Task<DailyStat?> GetByDateAsync(long? serviceId, DateOnly date)
|
||||
{
|
||||
return await _dbSet
|
||||
.FirstOrDefaultAsync(s => s.ServiceId == serviceId && s.StatDate == date);
|
||||
var query = _dbSet.Where(s => s.StatDate == date);
|
||||
if (serviceId.HasValue)
|
||||
query = query.Where(s => s.ServiceId == serviceId.Value);
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt)
|
||||
|
|
|
|||
|
|
@ -32,11 +32,13 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
|
|||
}
|
||||
|
||||
public async Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
|
||||
long serviceId, int page, int size,
|
||||
long? serviceId, int page, int size,
|
||||
Platform? platform = null, bool? pushAgreed = null,
|
||||
bool? isActive = null, List<int>? tags = null)
|
||||
{
|
||||
var query = _dbSet.Where(d => d.ServiceId == serviceId);
|
||||
IQueryable<Device> query = _dbSet;
|
||||
if (serviceId.HasValue)
|
||||
query = query.Where(d => d.ServiceId == serviceId.Value);
|
||||
|
||||
if (platform.HasValue)
|
||||
query = query.Where(d => d.Platform == platform.Value);
|
||||
|
|
|
|||
|
|
@ -45,10 +45,13 @@ public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogReposi
|
|||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long serviceId, DateTime startDate, DateTime endDate)
|
||||
public async Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long? serviceId, DateTime startDate, DateTime endDate)
|
||||
{
|
||||
return await _dbSet
|
||||
.Where(l => l.ServiceId == serviceId && l.SentAt >= startDate && l.SentAt < endDate)
|
||||
var query = _dbSet.Where(l => l.SentAt >= startDate && l.SentAt < endDate);
|
||||
if (serviceId.HasValue)
|
||||
query = query.Where(l => l.ServiceId == serviceId.Value);
|
||||
|
||||
return await query
|
||||
.GroupBy(l => l.SentAt.Hour)
|
||||
.Select(g => new HourlyStatRaw
|
||||
{
|
||||
|
|
@ -157,14 +160,16 @@ public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogReposi
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<FailureStatRaw>> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit)
|
||||
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
|
||||
var query = _dbSet.Where(l => l.Status == PushResult.Failed
|
||||
&& l.SentAt >= startDate
|
||||
&& l.SentAt < endDate
|
||||
&& l.FailReason != null)
|
||||
&& l.FailReason != null);
|
||||
if (serviceId.HasValue)
|
||||
query = query.Where(l => l.ServiceId == serviceId.Value);
|
||||
|
||||
return await query
|
||||
.GroupBy(l => l.FailReason!)
|
||||
.Select(g => new FailureStatRaw
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user