diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index 8fee579..fc968a7 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -72,7 +72,7 @@ public class ServiceController : ControllerBase [HttpPost("update")] [SwaggerOperation( Summary = "서비스 수정", - Description = "기존 서비스의 정보를 수정합니다. 서비스명, 설명, 웹훅 URL, 태그를 변경할 수 있습니다.")] + Description = "기존 서비스의 정보를 수정합니다. 서비스명, 설명, 웹훅 URL, 태그, 상태(Status)를 변경할 수 있습니다. Status 필드(0=Active, 1=Suspended)를 함께 전달하면 상태도 원자적으로 변경됩니다.")] [SwaggerResponse(200, "수정 성공", typeof(ApiResponse))] [SwaggerResponse(400, "변경된 내용 없음")] [SwaggerResponse(401, "인증되지 않은 요청")] @@ -192,10 +192,38 @@ public class ServiceController : ControllerBase return Ok(ApiResponse.Success()); } + [HttpPost("{serviceCode}/apns/delete")] + [SwaggerOperation( + Summary = "APNs 자격증명 삭제", + Description = "서비스에 등록된 APNs 자격증명(BundleId, KeyId, TeamId, PrivateKey, Certificate 등)을 모두 삭제합니다.")] + [SwaggerResponse(200, "삭제 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스 또는 자격증명을 찾을 수 없음")] + public async Task DeleteApnsCredentialsAsync([FromRoute] string serviceCode) + { + await _serviceManagementService.DeleteApnsCredentialsAsync(serviceCode); + return Ok(ApiResponse.Success()); + } + + [HttpPost("{serviceCode}/fcm/delete")] + [SwaggerOperation( + Summary = "FCM 자격증명 삭제", + Description = "서비스에 등록된 FCM Service Account JSON 자격증명을 삭제합니다.")] + [SwaggerResponse(200, "삭제 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스 또는 자격증명을 찾을 수 없음")] + public async Task DeleteFcmCredentialsAsync([FromRoute] string serviceCode) + { + await _serviceManagementService.DeleteFcmCredentialsAsync(serviceCode); + return Ok(ApiResponse.Success()); + } + [HttpPost("{serviceCode}/credentials")] [SwaggerOperation( Summary = "푸시 키 정보 조회", - Description = "서비스에 등록된 APNs/FCM 키의 메타 정보를 조회합니다. 민감 정보(Private Key)는 반환되지 않습니다.")] + Description = "서비스에 등록된 APNs/FCM 키의 메타 정보를 조회합니다. 민감 정보(Private Key)는 반환되지 않습니다. 각 플랫폼별 credentialStatus(ok/warn/error/none)와 statusReason 필드로 자격증명 상태 진단 결과를 제공합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] diff --git a/SPMS.Application/DTOs/Service/CredentialsResponseDto.cs b/SPMS.Application/DTOs/Service/CredentialsResponseDto.cs index 1722b25..d84023e 100644 --- a/SPMS.Application/DTOs/Service/CredentialsResponseDto.cs +++ b/SPMS.Application/DTOs/Service/CredentialsResponseDto.cs @@ -17,10 +17,16 @@ public class ApnsCredentialsInfoDto // p12 메타 public bool HasCertificate { get; set; } public DateTime? CertExpiresAt { get; set; } + // 진단 상태 + public string CredentialStatus { get; set; } = "none"; // ok|warn|error|none + public string? StatusReason { get; set; } } public class FcmCredentialsInfoDto { public string? ProjectId { get; set; } public bool HasCredentials { get; set; } + // 진단 상태 + public string CredentialStatus { get; set; } = "none"; // ok|none + public string? StatusReason { get; set; } } diff --git a/SPMS.Application/DTOs/Service/UpdateServiceRequestDto.cs b/SPMS.Application/DTOs/Service/UpdateServiceRequestDto.cs index 2ed7b55..c4984e6 100644 --- a/SPMS.Application/DTOs/Service/UpdateServiceRequestDto.cs +++ b/SPMS.Application/DTOs/Service/UpdateServiceRequestDto.cs @@ -17,4 +17,9 @@ public class UpdateServiceRequestDto public string? WebhookUrl { get; set; } public string? Tags { get; set; } + + /// + /// 서비스 상태 (0: Active, 1: Suspended). 제공 시 상태도 함께 변경됩니다. + /// + public int? Status { get; set; } } diff --git a/SPMS.Application/Interfaces/IServiceManagementService.cs b/SPMS.Application/Interfaces/IServiceManagementService.cs index ce47ee2..cee25ab 100644 --- a/SPMS.Application/Interfaces/IServiceManagementService.cs +++ b/SPMS.Application/Interfaces/IServiceManagementService.cs @@ -16,6 +16,8 @@ public interface IServiceManagementService Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request); Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request); Task GetCredentialsAsync(string serviceCode); + Task DeleteApnsCredentialsAsync(string serviceCode); + Task DeleteFcmCredentialsAsync(string serviceCode); // Tags Task GetTagsAsync(ServiceTagsRequestDto request); diff --git a/SPMS.Application/Services/ServiceManagementService.cs b/SPMS.Application/Services/ServiceManagementService.cs index c0332db..1822be2 100644 --- a/SPMS.Application/Services/ServiceManagementService.cs +++ b/SPMS.Application/Services/ServiceManagementService.cs @@ -137,6 +137,17 @@ public class ServiceManagementService : IServiceManagementService hasChanges = true; } + // Status 변경 + if (request.Status.HasValue) + { + var newStatus = (ServiceStatus)request.Status.Value; + if (service.Status != newStatus) + { + service.Status = newStatus; + hasChanges = true; + } + } + if (!hasChanges) { throw new SpmsException( @@ -627,7 +638,7 @@ public class ServiceManagementService : IServiceManagementService // APNs info (meta only, no private key) if (!string.IsNullOrEmpty(service.ApnsBundleId)) { - response.Apns = new ApnsCredentialsInfoDto + var apnsInfo = new ApnsCredentialsInfoDto { BundleId = service.ApnsBundleId, AuthType = service.ApnsAuthType, @@ -637,6 +648,16 @@ public class ServiceManagementService : IServiceManagementService HasCertificate = !string.IsNullOrEmpty(service.ApnsCertificate), CertExpiresAt = service.ApnsCertExpiresAt }; + + // 상태 진단 — BuildIosSummary 로직 재활용 + var iosSummary = BuildIosSummary(service); + if (iosSummary != null) + { + apnsInfo.CredentialStatus = iosSummary.CredentialStatus ?? "ok"; + apnsInfo.StatusReason = iosSummary.StatusReason; + } + + response.Apns = apnsInfo; } // FCM info (project_id only, no private key) @@ -661,13 +682,75 @@ public class ServiceManagementService : IServiceManagementService response.Fcm = new FcmCredentialsInfoDto { ProjectId = projectId, - HasCredentials = true + HasCredentials = true, + CredentialStatus = "ok" }; } return response; } + public async Task DeleteApnsCredentialsAsync(string serviceCode) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + // APNs 자격증명이 없으면 에러 + if (string.IsNullOrEmpty(service.ApnsBundleId)) + { + throw new SpmsException( + ErrorCodes.NotFound, + "삭제할 APNs 자격증명이 없습니다.", + 404); + } + + service.ApnsBundleId = null; + service.ApnsKeyId = null; + service.ApnsTeamId = null; + service.ApnsPrivateKey = null; + service.ApnsAuthType = null; + service.ApnsCertificate = null; + service.ApnsCertPassword = null; + service.ApnsCertExpiresAt = null; + service.UpdatedAt = DateTime.UtcNow; + + _serviceRepository.Update(service); + await _unitOfWork.SaveChangesAsync(); + } + + public async Task DeleteFcmCredentialsAsync(string serviceCode) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + // FCM 자격증명이 없으면 에러 + if (string.IsNullOrEmpty(service.FcmCredentials)) + { + throw new SpmsException( + ErrorCodes.NotFound, + "삭제할 FCM 자격증명이 없습니다.", + 404); + } + + service.FcmCredentials = null; + service.UpdatedAt = DateTime.UtcNow; + + _serviceRepository.Update(service); + await _unitOfWork.SaveChangesAsync(); + } + public async Task GetTagsAsync(ServiceTagsRequestDto request) { var service = await _serviceRepository.GetByServiceCodeAsync(request.ServiceCode);