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

- 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
This commit is contained in:
SEAN 2026-02-25 13:21:30 +09:00
parent e50f3f186c
commit e3ed3d4267
5 changed files with 125 additions and 3 deletions

View File

@ -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<ServiceListResponseDto>))]
[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<ServiceResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")]
[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 DateTime CreatedAt { 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? 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;

View File

@ -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<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;
}
}