From e3ed3d426744e80647ba331d8205390817fca1bf Mon Sep 17 00:00:00 2001 From: SEAN Date: Wed, 25 Feb 2026 13:21:30 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D/=EC=83=81=EC=84=B8=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=97=90=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlatformSummaryDto / PlatformCredentialSummaryDto 신규 생성 - ServiceSummaryDto에 Platforms 필드 추가 (목록 응답) - ServiceResponseDto에 ApnsAuthType + Platforms 필드 추가 (상세 응답) - BuildPlatformSummary 메서드로 Android/iOS 상태 판정 - Android: FcmCredentials 유무 → ok/none - iOS p8: → ok - iOS p12: 만료됨→error, 30일 이내→warn, 그 외→ok - Swagger Description 업데이트 Closes #216 --- SPMS.API/Controllers/ServiceController.cs | 4 +- .../DTOs/Service/PlatformSummaryDto.cs | 33 +++++++ .../DTOs/Service/ServiceListResponseDto.cs | 1 + .../DTOs/Service/ServiceResponseDto.cs | 2 + .../Services/ServiceManagementService.cs | 88 ++++++++++++++++++- 5 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 SPMS.Application/DTOs/Service/PlatformSummaryDto.cs diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index 01d28ab..8fee579 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -103,7 +103,7 @@ public class ServiceController : ControllerBase [HttpPost("list")] [SwaggerOperation( Summary = "서비스 목록 조회", - Description = "등록된 서비스 목록을 조회합니다. 페이징, 검색, 상태 필터를 지원합니다.")] + Description = "등록된 서비스 목록을 조회합니다. 페이징, 검색, 상태 필터를 지원합니다. 각 항목에 platforms 필드로 Android/iOS 자격증명 상태(credentialStatus: ok/warn/error)를 포함합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] @@ -116,7 +116,7 @@ public class ServiceController : ControllerBase [HttpPost("{serviceCode}")] [SwaggerOperation( Summary = "서비스 상세 조회", - Description = "특정 서비스의 상세 정보를 조회합니다.")] + Description = "특정 서비스의 상세 정보를 조회합니다. apnsAuthType(p8/p12)과 platforms 필드로 각 플랫폼의 자격증명 상태(credentialStatus: ok/warn/error), 만료일(expiresAt) 정보를 포함합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] diff --git a/SPMS.Application/DTOs/Service/PlatformSummaryDto.cs b/SPMS.Application/DTOs/Service/PlatformSummaryDto.cs new file mode 100644 index 0000000..0369d97 --- /dev/null +++ b/SPMS.Application/DTOs/Service/PlatformSummaryDto.cs @@ -0,0 +1,33 @@ +namespace SPMS.Application.DTOs.Service; + +/// +/// 서비스의 플랫폼(Android/iOS) 자격증명 상태 요약 +/// +public class PlatformSummaryDto +{ + public PlatformCredentialSummaryDto? Android { get; set; } + public PlatformCredentialSummaryDto? Ios { get; set; } +} + +/// +/// 개별 플랫폼 자격증명 상태 +/// +public class PlatformCredentialSummaryDto +{ + public bool Registered { get; set; } + + /// + /// 자격증명 상태: ok | warn | error | none + /// + public string CredentialStatus { get; set; } = "none"; + + /// + /// 상태 사유 (warn/error 시 표시) + /// + public string? StatusReason { get; set; } + + /// + /// p12 인증서 만료일 (p12 타입만 해당) + /// + public DateTime? ExpiresAt { get; set; } +} diff --git a/SPMS.Application/DTOs/Service/ServiceListResponseDto.cs b/SPMS.Application/DTOs/Service/ServiceListResponseDto.cs index db585f2..115b517 100644 --- a/SPMS.Application/DTOs/Service/ServiceListResponseDto.cs +++ b/SPMS.Application/DTOs/Service/ServiceListResponseDto.cs @@ -18,4 +18,5 @@ public class ServiceSummaryDto public string Status { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } public int DeviceCount { get; set; } + public PlatformSummaryDto? Platforms { get; set; } } diff --git a/SPMS.Application/DTOs/Service/ServiceResponseDto.cs b/SPMS.Application/DTOs/Service/ServiceResponseDto.cs index bc473ca..a41cfe5 100644 --- a/SPMS.Application/DTOs/Service/ServiceResponseDto.cs +++ b/SPMS.Application/DTOs/Service/ServiceResponseDto.cs @@ -10,8 +10,10 @@ public class ServiceResponseDto public string? ApnsBundleId { get; set; } public string? ApnsKeyId { get; set; } public string? ApnsTeamId { get; set; } + public string? ApnsAuthType { get; set; } public bool HasApnsKey { get; set; } public bool HasFcmCredentials { get; set; } + public PlatformSummaryDto? Platforms { get; set; } public string? WebhookUrl { get; set; } public string? Tags { get; set; } public string SubTier { get; set; } = string.Empty; diff --git a/SPMS.Application/Services/ServiceManagementService.cs b/SPMS.Application/Services/ServiceManagementService.cs index ec38a1d..c0332db 100644 --- a/SPMS.Application/Services/ServiceManagementService.cs +++ b/SPMS.Application/Services/ServiceManagementService.cs @@ -886,7 +886,8 @@ public class ServiceManagementService : IServiceManagementService SubTier = service.SubTier.ToString(), Status = service.Status.ToString(), CreatedAt = service.CreatedAt, - DeviceCount = service.Devices?.Count ?? 0 + DeviceCount = service.Devices?.Count ?? 0, + Platforms = BuildPlatformSummary(service) }; } @@ -902,8 +903,10 @@ public class ServiceManagementService : IServiceManagementService ApnsBundleId = service.ApnsBundleId, ApnsKeyId = service.ApnsKeyId, ApnsTeamId = service.ApnsTeamId, + ApnsAuthType = service.ApnsAuthType, HasApnsKey = !string.IsNullOrEmpty(service.ApnsPrivateKey), HasFcmCredentials = !string.IsNullOrEmpty(service.FcmCredentials), + Platforms = BuildPlatformSummary(service), WebhookUrl = service.WebhookUrl, Tags = service.Tags, SubTier = service.SubTier.ToString(), @@ -916,4 +919,87 @@ public class ServiceManagementService : IServiceManagementService AllowedIps = service.ServiceIps?.Select(ip => ip.IpAddress).ToList() ?? new List() }; } + + /// + /// 서비스의 플랫폼 자격증명 상태를 판정하여 PlatformSummaryDto를 반환합니다. + /// Android: FcmCredentials 유무로 판정 + /// iOS: ApnsAuthType(p8/p12)에 따라 만료 상태 포함 판정 + /// + private static PlatformSummaryDto? BuildPlatformSummary(Service service) + { + var android = BuildAndroidSummary(service); + var ios = BuildIosSummary(service); + + // 양쪽 다 null이면 null 반환 + if (android == null && ios == null) + return null; + + return new PlatformSummaryDto + { + Android = android, + Ios = ios + }; + } + + private static PlatformCredentialSummaryDto? BuildAndroidSummary(Service service) + { + if (string.IsNullOrEmpty(service.FcmCredentials)) + return null; + + return new PlatformCredentialSummaryDto + { + Registered = true, + CredentialStatus = "ok" + }; + } + + private static PlatformCredentialSummaryDto? BuildIosSummary(Service service) + { + // APNs 미등록 + if (string.IsNullOrEmpty(service.ApnsBundleId)) + return null; + + var summary = new PlatformCredentialSummaryDto + { + Registered = true + }; + + if (service.ApnsAuthType == "p12") + { + var now = DateTime.UtcNow; + + if (service.ApnsCertExpiresAt.HasValue) + { + if (service.ApnsCertExpiresAt.Value < now) + { + summary.CredentialStatus = "error"; + summary.StatusReason = "p12 인증서가 만료되었습니다."; + } + else if (service.ApnsCertExpiresAt.Value < now.AddDays(30)) + { + summary.CredentialStatus = "warn"; + summary.StatusReason = "p12 인증서 만료가 30일 이내입니다."; + } + else + { + summary.CredentialStatus = "ok"; + } + + summary.ExpiresAt = service.ApnsCertExpiresAt; + } + else + { + // p12인데 만료일 정보 없음 (비정상) + summary.CredentialStatus = "warn"; + summary.StatusReason = "p12 인증서 만료일 정보가 없습니다."; + } + } + else + { + // p8 또는 레거시(AuthType null + PrivateKey 존재) + summary.CredentialStatus = "ok"; + } + + return summary; + } } -- 2.45.1