From a3b2da5ffbb1279b99aa30271069900dd4569c2b Mon Sep 17 00:00:00 2001 From: SEAN Date: Wed, 25 Feb 2026 15:47:02 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20=ED=86=B5=EA=B3=84=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=B2=94=EC=9C=84=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20(#229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stats 도메인 에러코드 추가 (171: DateRangeInvalid, 172: ServiceScopeInvalid) - StatsService ParseDateRange에서 generic BadRequest → StatsDateRangeInvalid로 교체 - StatsController 전 엔드포인트 Swagger Description에 스코프 정책 안내 추가 - SpmsHeaderOperationFilter에서 message/list를 Optional로 반영 (미들웨어 정합) Closes #229 --- SPMS.API/Controllers/StatsController.cs | 16 ++++++++-------- SPMS.API/Filters/SpmsHeaderOperationFilter.cs | 11 ++++++----- SPMS.Application/Services/StatsService.cs | 6 +++--- SPMS.Domain/Common/ErrorCodes.cs | 4 ++++ 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/SPMS.API/Controllers/StatsController.cs b/SPMS.API/Controllers/StatsController.cs index 7fe6391..544d566 100644 --- a/SPMS.API/Controllers/StatsController.cs +++ b/SPMS.API/Controllers/StatsController.cs @@ -19,7 +19,7 @@ public class StatsController : ControllerBase } [HttpPost("daily")] - [SwaggerOperation(Summary = "일별 통계 조회", Description = "기간별 일별 발송/성공/실패/열람 통계를 조회합니다.")] + [SwaggerOperation(Summary = "일별 통계 조회", Description = "기간별 일별 발송/성공/실패/열람 통계를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetDailyAsync([FromBody] DailyStatRequestDto request) { var serviceId = GetOptionalServiceId(); @@ -28,7 +28,7 @@ public class StatsController : ControllerBase } [HttpPost("summary")] - [SwaggerOperation(Summary = "요약 통계 조회", Description = "대시보드 요약 통계를 조회합니다.")] + [SwaggerOperation(Summary = "요약 통계 조회", Description = "대시보드 요약 통계를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetSummaryAsync() { var serviceId = GetOptionalServiceId(); @@ -37,7 +37,7 @@ public class StatsController : ControllerBase } [HttpPost("message")] - [SwaggerOperation(Summary = "메시지별 통계 조회", Description = "특정 메시지의 발송 통계를 조회합니다.")] + [SwaggerOperation(Summary = "메시지별 통계 조회", Description = "특정 메시지의 발송 통계를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetMessageStatAsync([FromBody] MessageStatRequestDto request) { var serviceId = GetOptionalServiceId(); @@ -46,7 +46,7 @@ public class StatsController : ControllerBase } [HttpPost("hourly")] - [SwaggerOperation(Summary = "시간대별 통계 조회", Description = "시간대별 발송 추이를 조회합니다.")] + [SwaggerOperation(Summary = "시간대별 통계 조회", Description = "시간대별 발송 추이를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetHourlyAsync([FromBody] HourlyStatRequestDto request) { var serviceId = GetOptionalServiceId(); @@ -55,7 +55,7 @@ public class StatsController : ControllerBase } [HttpPost("device")] - [SwaggerOperation(Summary = "디바이스 통계 조회", Description = "플랫폼/모델별 디바이스 분포를 조회합니다.")] + [SwaggerOperation(Summary = "디바이스 통계 조회", Description = "플랫폼/모델별 디바이스 분포를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetDeviceStatAsync() { var serviceId = GetOptionalServiceId(); @@ -64,7 +64,7 @@ public class StatsController : ControllerBase } [HttpPost("export")] - [SwaggerOperation(Summary = "통계 리포트 다운로드", Description = "일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 파일로 다운로드합니다.")] + [SwaggerOperation(Summary = "통계 리포트 다운로드", Description = "일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 파일로 다운로드합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task ExportReportAsync([FromBody] StatsExportRequestDto request) { var serviceId = GetOptionalServiceId(); @@ -74,7 +74,7 @@ public class StatsController : ControllerBase } [HttpPost("failure")] - [SwaggerOperation(Summary = "실패원인 통계 조회", Description = "실패 원인별 집계를 상위 N개로 조회합니다.")] + [SwaggerOperation(Summary = "실패원인 통계 조회", Description = "실패 원인별 집계를 상위 N개로 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetFailureStatAsync([FromBody] FailureStatRequestDto request) { var serviceId = GetOptionalServiceId(); @@ -83,7 +83,7 @@ public class StatsController : ControllerBase } [HttpPost("send-log")] - [SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")] + [SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다. X-Service-Code 헤더 미지정 시 전체 서비스 통계를 조회합니다.")] public async Task GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request) { var serviceId = GetOptionalServiceId(); diff --git a/SPMS.API/Filters/SpmsHeaderOperationFilter.cs b/SPMS.API/Filters/SpmsHeaderOperationFilter.cs index 45fc3c4..fbe4da5 100644 --- a/SPMS.API/Filters/SpmsHeaderOperationFilter.cs +++ b/SPMS.API/Filters/SpmsHeaderOperationFilter.cs @@ -16,14 +16,15 @@ public class SpmsHeaderOperationFilter : IOperationFilter operation.Parameters ??= new List(); // v1/in/* 중 X-Service-Code 대상 경로 판별 - var isStatsOrDeviceList = relativePath.StartsWith("v1/in/stats") || - relativePath == "v1/in/device/list"; - var isRequired = relativePath.StartsWith("v1/in/message") || + var isOptional = relativePath.StartsWith("v1/in/stats") || + relativePath == "v1/in/device/list" || + relativePath == "v1/in/message/list"; + var isRequired = (relativePath.StartsWith("v1/in/message") && !isOptional) || relativePath.StartsWith("v1/in/push") || relativePath.StartsWith("v1/in/file"); - var isDeviceNonList = relativePath.StartsWith("v1/in/device") && !isStatsOrDeviceList; + var isDeviceNonList = relativePath.StartsWith("v1/in/device") && !isOptional; - if (isStatsOrDeviceList) + if (isOptional) { operation.Parameters.Add(new OpenApiParameter { diff --git a/SPMS.Application/Services/StatsService.cs b/SPMS.Application/Services/StatsService.cs index 8837b1e..6fb144b 100644 --- a/SPMS.Application/Services/StatsService.cs +++ b/SPMS.Application/Services/StatsService.cs @@ -443,13 +443,13 @@ public class StatsService : IStatsService private static (DateOnly Start, DateOnly End) ParseDateRange(string startStr, string endStr) { if (!DateOnly.TryParseExact(startStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var start)) - throw new SpmsException(ErrorCodes.BadRequest, "start_date 형식이 올바르지 않습니다. (yyyy-MM-dd)", 400); + throw new SpmsException(ErrorCodes.StatsDateRangeInvalid, "start_date 형식이 올바르지 않습니다. (yyyy-MM-dd)", 400); if (!DateOnly.TryParseExact(endStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var end)) - throw new SpmsException(ErrorCodes.BadRequest, "end_date 형식이 올바르지 않습니다. (yyyy-MM-dd)", 400); + throw new SpmsException(ErrorCodes.StatsDateRangeInvalid, "end_date 형식이 올바르지 않습니다. (yyyy-MM-dd)", 400); if (start > end) - throw new SpmsException(ErrorCodes.BadRequest, "start_date가 end_date보다 클 수 없습니다.", 400); + throw new SpmsException(ErrorCodes.StatsDateRangeInvalid, "start_date가 end_date보다 클 수 없습니다.", 400); return (start, end); } diff --git a/SPMS.Domain/Common/ErrorCodes.cs b/SPMS.Domain/Common/ErrorCodes.cs index 76ee6fc..96bc07d 100644 --- a/SPMS.Domain/Common/ErrorCodes.cs +++ b/SPMS.Domain/Common/ErrorCodes.cs @@ -51,6 +51,10 @@ public static class ErrorCodes public const string JobNotFound = "163"; public const string JobAlreadyCompleted = "164"; + // === Stats (7) === + public const string StatsDateRangeInvalid = "171"; + public const string StatsServiceScopeInvalid = "172"; + // === File (8) === public const string FileNotFound = "181"; public const string FileTypeNotAllowed = "182";