improvement: 서비스 목록/상세 응답에 플랫폼 상태 판정 추가 (#216) #217

Merged
seonkyu.kim merged 1 commits from improvement/#216-service-list-detail-platform-summary into develop 2026-02-25 04:23:54 +00:00
5 changed files with 125 additions and 3 deletions

View File

@ -103,7 +103,7 @@ public class ServiceController : ControllerBase
[HttpPost("list")] [HttpPost("list")]
[SwaggerOperation( [SwaggerOperation(
Summary = "서비스 목록 조회", Summary = "서비스 목록 조회",
Description = "등록된 서비스 목록을 조회합니다. 페이징, 검색, 상태 필터를 지원합니다.")] Description = "등록된 서비스 목록을 조회합니다. 페이징, 검색, 상태 필터를 지원합니다. 각 항목에 platforms 필드로 Android/iOS 자격증명 상태(credentialStatus: ok/warn/error)를 포함합니다.")]
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceListResponseDto>))] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceListResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(401, "인증되지 않은 요청")]
[SwaggerResponse(403, "권한 없음")] [SwaggerResponse(403, "권한 없음")]
@ -116,7 +116,7 @@ public class ServiceController : ControllerBase
[HttpPost("{serviceCode}")] [HttpPost("{serviceCode}")]
[SwaggerOperation( [SwaggerOperation(
Summary = "서비스 상세 조회", Summary = "서비스 상세 조회",
Description = "특정 서비스의 상세 정보를 조회합니다.")] Description = "특정 서비스의 상세 정보를 조회합니다. apnsAuthType(p8/p12)과 platforms 필드로 각 플랫폼의 자격증명 상태(credentialStatus: ok/warn/error), 만료일(expiresAt) 정보를 포함합니다.")]
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceResponseDto>))] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(401, "인증되지 않은 요청")]
[SwaggerResponse(403, "권한 없음")] [SwaggerResponse(403, "권한 없음")]

View File

@ -0,0 +1,33 @@
namespace SPMS.Application.DTOs.Service;
/// <summary>
/// 서비스의 플랫폼(Android/iOS) 자격증명 상태 요약
/// </summary>
public class PlatformSummaryDto
{
public PlatformCredentialSummaryDto? Android { get; set; }
public PlatformCredentialSummaryDto? Ios { get; set; }
}
/// <summary>
/// 개별 플랫폼 자격증명 상태
/// </summary>
public class PlatformCredentialSummaryDto
{
public bool Registered { get; set; }
/// <summary>
/// 자격증명 상태: ok | warn | error | none
/// </summary>
public string CredentialStatus { get; set; } = "none";
/// <summary>
/// 상태 사유 (warn/error 시 표시)
/// </summary>
public string? StatusReason { get; set; }
/// <summary>
/// p12 인증서 만료일 (p12 타입만 해당)
/// </summary>
public DateTime? ExpiresAt { get; set; }
}

View File

@ -18,4 +18,5 @@ public class ServiceSummaryDto
public string Status { get; set; } = string.Empty; public string Status { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public int DeviceCount { get; set; } public int DeviceCount { get; set; }
public PlatformSummaryDto? Platforms { get; set; }
} }

View File

@ -10,8 +10,10 @@ public class ServiceResponseDto
public string? ApnsBundleId { get; set; } public string? ApnsBundleId { get; set; }
public string? ApnsKeyId { get; set; } public string? ApnsKeyId { get; set; }
public string? ApnsTeamId { get; set; } public string? ApnsTeamId { get; set; }
public string? ApnsAuthType { get; set; }
public bool HasApnsKey { get; set; } public bool HasApnsKey { get; set; }
public bool HasFcmCredentials { get; set; } public bool HasFcmCredentials { get; set; }
public PlatformSummaryDto? Platforms { get; set; }
public string? WebhookUrl { get; set; } public string? WebhookUrl { get; set; }
public string? Tags { get; set; } public string? Tags { get; set; }
public string SubTier { get; set; } = string.Empty; public string SubTier { get; set; } = string.Empty;

View File

@ -886,7 +886,8 @@ public class ServiceManagementService : IServiceManagementService
SubTier = service.SubTier.ToString(), SubTier = service.SubTier.ToString(),
Status = service.Status.ToString(), Status = service.Status.ToString(),
CreatedAt = service.CreatedAt, 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, ApnsBundleId = service.ApnsBundleId,
ApnsKeyId = service.ApnsKeyId, ApnsKeyId = service.ApnsKeyId,
ApnsTeamId = service.ApnsTeamId, ApnsTeamId = service.ApnsTeamId,
ApnsAuthType = service.ApnsAuthType,
HasApnsKey = !string.IsNullOrEmpty(service.ApnsPrivateKey), HasApnsKey = !string.IsNullOrEmpty(service.ApnsPrivateKey),
HasFcmCredentials = !string.IsNullOrEmpty(service.FcmCredentials), HasFcmCredentials = !string.IsNullOrEmpty(service.FcmCredentials),
Platforms = BuildPlatformSummary(service),
WebhookUrl = service.WebhookUrl, WebhookUrl = service.WebhookUrl,
Tags = service.Tags, Tags = service.Tags,
SubTier = service.SubTier.ToString(), SubTier = service.SubTier.ToString(),
@ -916,4 +919,87 @@ public class ServiceManagementService : IServiceManagementService
AllowedIps = service.ServiceIps?.Select(ip => ip.IpAddress).ToList() ?? new List<string>() AllowedIps = service.ServiceIps?.Select(ip => ip.IpAddress).ToList() ?? new List<string>()
}; };
} }
/// <summary>
/// 서비스의 플랫폼 자격증명 상태를 판정하여 PlatformSummaryDto를 반환합니다.
/// Android: FcmCredentials 유무로 판정
/// iOS: ApnsAuthType(p8/p12)에 따라 만료 상태 포함 판정
/// </summary>
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;
}
} }