From 17caeb08e25149a38751284dfce7d1698eaffb77 Mon Sep 17 00:00:00 2001 From: SEAN Date: Wed, 25 Feb 2026 13:36:47 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20=EC=88=98=EC=A0=95/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C/=EC=A7=84=EB=8B=A8=20=EA=B3=84=EC=95=BD=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 플랫폼 자격증명 삭제 API 추가 (APNs/FCM 각각) - 자격증명 진단 응답에 credentialStatus/statusReason 추가 - 수정 API에 Status 필드 추가 (원자적 상태 변경) - Swagger Description 업데이트 Closes #218 --- SPMS.API/Controllers/ServiceController.cs | 32 ++++++- .../DTOs/Service/CredentialsResponseDto.cs | 6 ++ .../DTOs/Service/UpdateServiceRequestDto.cs | 5 ++ .../Interfaces/IServiceManagementService.cs | 2 + .../Services/ServiceManagementService.cs | 87 ++++++++++++++++++- 5 files changed, 128 insertions(+), 4 deletions(-) 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);