From f04eb44fffb0d0ad1fc42a62eb07c8ed77d2275c Mon Sep 17 00:00:00 2001 From: SEAN Date: Tue, 24 Feb 2026 17:11:30 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=8A=A4=EC=BD=94=ED=94=84=20=EC=A0=95=EC=B1=85=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorCodes.ServiceScopeRequired("133") 추가 - SpmsException.Forbidden 팩토리 추가 - ServiceCodeMiddleware 3-카테고리 라우팅 (SKIP/REQUIRED/OPTIONAL_FOR_ADMIN) - Swagger 필터 stats/device-list X-Service-Code optional 표시 - StatsController/DeviceController GetOptionalServiceId() 적용 - IStatsService/IDeviceService/레포지토리 시그니처 long? serviceId 변경 - StatsService/DeviceService null serviceId 전체 서비스 모드 처리 Closes #199 --- SPMS.API/Controllers/DeviceController.cs | 9 ++- SPMS.API/Controllers/StatsController.cs | 25 ++++---- SPMS.API/Filters/SpmsHeaderOperationFilter.cs | 26 +++++++-- SPMS.API/Middlewares/ServiceCodeMiddleware.cs | 57 +++++++++++++++---- SPMS.Application/Interfaces/IDeviceService.cs | 2 +- SPMS.Application/Interfaces/IStatsService.cs | 16 +++--- SPMS.Application/Services/DeviceService.cs | 2 +- SPMS.Application/Services/StatsService.cs | 54 +++++++++++------- SPMS.Domain/Common/ErrorCodes.cs | 1 + SPMS.Domain/Exceptions/SpmsException.cs | 3 + .../Interfaces/IDailyStatRepository.cs | 4 +- SPMS.Domain/Interfaces/IDeviceRepository.cs | 2 +- .../Interfaces/IPushSendLogRepository.cs | 4 +- .../Repositories/DailyStatRepository.cs | 18 +++--- .../Repositories/DeviceRepository.cs | 6 +- .../Repositories/PushSendLogRepository.cs | 25 ++++---- 16 files changed, 170 insertions(+), 84 deletions(-) diff --git a/SPMS.API/Controllers/DeviceController.cs b/SPMS.API/Controllers/DeviceController.cs index 77112cd..093c7bc 100644 --- a/SPMS.API/Controllers/DeviceController.cs +++ b/SPMS.API/Controllers/DeviceController.cs @@ -78,7 +78,7 @@ public class DeviceController : ControllerBase [SwaggerOperation(Summary = "디바이스 목록", Description = "대시보드에서 디바이스 목록을 조회합니다. JWT 인증 필요.")] public async Task GetListAsync([FromBody] DeviceListRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _deviceService.GetListAsync(serviceId, request); return Ok(ApiResponse.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; + } } diff --git a/SPMS.API/Controllers/StatsController.cs b/SPMS.API/Controllers/StatsController.cs index 724e29f..7fe6391 100644 --- a/SPMS.API/Controllers/StatsController.cs +++ b/SPMS.API/Controllers/StatsController.cs @@ -22,7 +22,7 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "일별 통계 조회", Description = "기간별 일별 발송/성공/실패/열람 통계를 조회합니다.")] public async Task GetDailyAsync([FromBody] DailyStatRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetDailyAsync(serviceId, request); return Ok(ApiResponse.Success(result, "조회 성공")); } @@ -31,7 +31,7 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "요약 통계 조회", Description = "대시보드 요약 통계를 조회합니다.")] public async Task GetSummaryAsync() { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetSummaryAsync(serviceId); return Ok(ApiResponse.Success(result, "조회 성공")); } @@ -40,7 +40,7 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "메시지별 통계 조회", Description = "특정 메시지의 발송 통계를 조회합니다.")] public async Task GetMessageStatAsync([FromBody] MessageStatRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetMessageStatAsync(serviceId, request); return Ok(ApiResponse.Success(result, "조회 성공")); } @@ -49,7 +49,7 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "시간대별 통계 조회", Description = "시간대별 발송 추이를 조회합니다.")] public async Task GetHourlyAsync([FromBody] HourlyStatRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetHourlyAsync(serviceId, request); return Ok(ApiResponse.Success(result, "조회 성공")); } @@ -58,7 +58,7 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "디바이스 통계 조회", Description = "플랫폼/모델별 디바이스 분포를 조회합니다.")] public async Task GetDeviceStatAsync() { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetDeviceStatAsync(serviceId); return Ok(ApiResponse.Success(result, "조회 성공")); } @@ -67,7 +67,7 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "통계 리포트 다운로드", Description = "일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 파일로 다운로드합니다.")] public async Task 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 GetFailureStatAsync([FromBody] FailureStatRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetFailureStatAsync(serviceId, request); return Ok(ApiResponse.Success(result, "조회 성공")); } @@ -86,16 +86,15 @@ public class StatsController : ControllerBase [SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")] public async Task GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetOptionalServiceId(); var result = await _statsService.GetSendLogDetailAsync(serviceId, request); return Ok(ApiResponse.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; } } diff --git a/SPMS.API/Filters/SpmsHeaderOperationFilter.cs b/SPMS.API/Filters/SpmsHeaderOperationFilter.cs index d3f538c..45fc3c4 100644 --- a/SPMS.API/Filters/SpmsHeaderOperationFilter.cs +++ b/SPMS.API/Filters/SpmsHeaderOperationFilter.cs @@ -15,12 +15,26 @@ public class SpmsHeaderOperationFilter : IOperationFilter operation.Parameters ??= new List(); - // v1/in/* 중 X-Service-Code가 필요한 경로만 (device, message, push, stats, file) - if (relativePath.StartsWith("v1/in/device") || - relativePath.StartsWith("v1/in/message") || - relativePath.StartsWith("v1/in/push") || - relativePath.StartsWith("v1/in/stats") || - relativePath.StartsWith("v1/in/file")) + // 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/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 { diff --git a/SPMS.API/Middlewares/ServiceCodeMiddleware.cs b/SPMS.API/Middlewares/ServiceCodeMiddleware.cs index 0359742..781d481 100644 --- a/SPMS.API/Middlewares/ServiceCodeMiddleware.cs +++ b/SPMS.API/Middlewares/ServiceCodeMiddleware.cs @@ -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; diff --git a/SPMS.Application/Interfaces/IDeviceService.cs b/SPMS.Application/Interfaces/IDeviceService.cs index 9de2ff6..b2cdb44 100644 --- a/SPMS.Application/Interfaces/IDeviceService.cs +++ b/SPMS.Application/Interfaces/IDeviceService.cs @@ -8,7 +8,7 @@ public interface IDeviceService Task GetInfoAsync(long serviceId, DeviceInfoRequestDto request); Task UpdateAsync(long serviceId, DeviceUpdateRequestDto request); Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request); - Task GetListAsync(long serviceId, DeviceListRequestDto request); + Task GetListAsync(long? serviceId, DeviceListRequestDto request); Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request); Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request); } diff --git a/SPMS.Application/Interfaces/IStatsService.cs b/SPMS.Application/Interfaces/IStatsService.cs index 10a2877..c71eb42 100644 --- a/SPMS.Application/Interfaces/IStatsService.cs +++ b/SPMS.Application/Interfaces/IStatsService.cs @@ -4,12 +4,12 @@ namespace SPMS.Application.Interfaces; public interface IStatsService { - Task GetDailyAsync(long serviceId, DailyStatRequestDto request); - Task GetSummaryAsync(long serviceId); - Task GetMessageStatAsync(long serviceId, MessageStatRequestDto request); - Task GetHourlyAsync(long serviceId, HourlyStatRequestDto request); - Task GetDeviceStatAsync(long serviceId); - Task GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request); - Task ExportReportAsync(long serviceId, StatsExportRequestDto request); - Task GetFailureStatAsync(long serviceId, FailureStatRequestDto request); + Task GetDailyAsync(long? serviceId, DailyStatRequestDto request); + Task GetSummaryAsync(long? serviceId); + Task GetMessageStatAsync(long? serviceId, MessageStatRequestDto request); + Task GetHourlyAsync(long? serviceId, HourlyStatRequestDto request); + Task GetDeviceStatAsync(long? serviceId); + Task GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request); + Task ExportReportAsync(long? serviceId, StatsExportRequestDto request); + Task GetFailureStatAsync(long? serviceId, FailureStatRequestDto request); } diff --git a/SPMS.Application/Services/DeviceService.cs b/SPMS.Application/Services/DeviceService.cs index 51f8b75..afbfcd7 100644 --- a/SPMS.Application/Services/DeviceService.cs +++ b/SPMS.Application/Services/DeviceService.cs @@ -119,7 +119,7 @@ public class DeviceService : IDeviceService await _tokenCache.InvalidateAsync(serviceId, device.Id); } - public async Task GetListAsync(long serviceId, DeviceListRequestDto request) + public async Task GetListAsync(long? serviceId, DeviceListRequestDto request) { Platform? platform = null; if (!string.IsNullOrWhiteSpace(request.Platform)) diff --git a/SPMS.Application/Services/StatsService.cs b/SPMS.Application/Services/StatsService.cs index e194cae..8837b1e 100644 --- a/SPMS.Application/Services/StatsService.cs +++ b/SPMS.Application/Services/StatsService.cs @@ -29,7 +29,7 @@ public class StatsService : IStatsService _messageRepository = messageRepository; } - public async Task GetDailyAsync(long serviceId, DailyStatRequestDto request) + public async Task GetDailyAsync(long? serviceId, DailyStatRequestDto request) { var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate); @@ -64,13 +64,21 @@ public class StatsService : IStatsService }; } - public async Task GetSummaryAsync(long serviceId) + public async Task 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 GetMessageStatAsync(long serviceId, MessageStatRequestDto request) + public async Task 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 GetHourlyAsync(long serviceId, HourlyStatRequestDto request) + public async Task GetHourlyAsync(long? serviceId, HourlyStatRequestDto request) { var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate); @@ -178,9 +188,11 @@ public class StatsService : IStatsService }; } - public async Task GetDeviceStatAsync(long serviceId) + public async Task 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 GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request) + public async Task 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 ExportReportAsync(long serviceId, StatsExportRequestDto request) + public async Task 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 GetFailureStatAsync(long serviceId, FailureStatRequestDto request) + public async Task GetFailureStatAsync(long? serviceId, FailureStatRequestDto request) { var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate); diff --git a/SPMS.Domain/Common/ErrorCodes.cs b/SPMS.Domain/Common/ErrorCodes.cs index ef87aa1..41438ce 100644 --- a/SPMS.Domain/Common/ErrorCodes.cs +++ b/SPMS.Domain/Common/ErrorCodes.cs @@ -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"; diff --git a/SPMS.Domain/Exceptions/SpmsException.cs b/SPMS.Domain/Exceptions/SpmsException.cs index 23ae4b0..374d0e8 100644 --- a/SPMS.Domain/Exceptions/SpmsException.cs +++ b/SPMS.Domain/Exceptions/SpmsException.cs @@ -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); } diff --git a/SPMS.Domain/Interfaces/IDailyStatRepository.cs b/SPMS.Domain/Interfaces/IDailyStatRepository.cs index 614f060..cd6146b 100644 --- a/SPMS.Domain/Interfaces/IDailyStatRepository.cs +++ b/SPMS.Domain/Interfaces/IDailyStatRepository.cs @@ -4,7 +4,7 @@ namespace SPMS.Domain.Interfaces; public interface IDailyStatRepository : IRepository { - Task> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate); - Task GetByDateAsync(long serviceId, DateOnly date); + Task> GetByDateRangeAsync(long? serviceId, DateOnly startDate, DateOnly endDate); + Task GetByDateAsync(long? serviceId, DateOnly date); Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt); } diff --git a/SPMS.Domain/Interfaces/IDeviceRepository.cs b/SPMS.Domain/Interfaces/IDeviceRepository.cs index 5744792..57a28e8 100644 --- a/SPMS.Domain/Interfaces/IDeviceRepository.cs +++ b/SPMS.Domain/Interfaces/IDeviceRepository.cs @@ -10,7 +10,7 @@ public interface IDeviceRepository : IRepository Task GetActiveCountByServiceAsync(long serviceId); Task> GetByPlatformAsync(long serviceId, Platform platform); Task<(IReadOnlyList 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? tags = null); } diff --git a/SPMS.Domain/Interfaces/IPushSendLogRepository.cs b/SPMS.Domain/Interfaces/IPushSendLogRepository.cs index a5a98da..4fc99f4 100644 --- a/SPMS.Domain/Interfaces/IPushSendLogRepository.cs +++ b/SPMS.Domain/Interfaces/IPushSendLogRepository.cs @@ -11,7 +11,7 @@ public interface IPushSendLogRepository : IRepository PushResult? status = null, DateTime? startDate = null, DateTime? endDate = null); - Task> GetHourlyStatsAsync(long serviceId, DateTime startDate, DateTime endDate); + Task> GetHourlyStatsAsync(long? serviceId, DateTime startDate, DateTime endDate); Task GetMessageStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate); Task> GetMessageDailyStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate); Task<(IReadOnlyList Items, int TotalCount)> GetDetailLogPagedAsync( @@ -21,7 +21,7 @@ public interface IPushSendLogRepository : IRepository long serviceId, DateTime startDate, DateTime endDate, long? messageId = null, long? deviceId = null, PushResult? status = null, int maxCount = 100000); - Task> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit); + Task> GetFailureStatsAsync(long? serviceId, DateTime startDate, DateTime endDate, int limit); } public class HourlyStatRaw diff --git a/SPMS.Infrastructure/Persistence/Repositories/DailyStatRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/DailyStatRepository.cs index c1d8591..d280fa8 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/DailyStatRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/DailyStatRepository.cs @@ -8,18 +8,20 @@ public class DailyStatRepository : Repository, IDailyStatRepository { public DailyStatRepository(AppDbContext context) : base(context) { } - public async Task> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate) + public async Task> 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 GetByDateAsync(long serviceId, DateOnly date) + public async Task 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) diff --git a/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs index a327799..c8f1e13 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs @@ -32,11 +32,13 @@ public class DeviceRepository : Repository, IDeviceRepository } public async Task<(IReadOnlyList 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? tags = null) { - var query = _dbSet.Where(d => d.ServiceId == serviceId); + IQueryable query = _dbSet; + if (serviceId.HasValue) + query = query.Where(d => d.ServiceId == serviceId.Value); if (platform.HasValue) query = query.Where(d => d.Platform == platform.Value); diff --git a/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs index 7f4b4de..e6b4d6d 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs @@ -45,10 +45,13 @@ public class PushSendLogRepository : Repository, IPushSendLogReposi return (items, totalCount); } - public async Task> GetHourlyStatsAsync(long serviceId, DateTime startDate, DateTime endDate) + public async Task> 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, IPushSendLogReposi .ToListAsync(); } - public async Task> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit) + public async Task> 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) + var query = _dbSet.Where(l => l.Status == PushResult.Failed + && l.SentAt >= startDate + && l.SentAt < endDate + && 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 { -- 2.45.1