improvement: 수정/삭제/진단 계약 확장 (#218) #219

Merged
seonkyu.kim merged 1 commits from improvement/#218-service-edit-delete-diagnosis into develop 2026-02-25 04:38:12 +00:00
5 changed files with 128 additions and 4 deletions
Showing only changes of commit 17caeb08e2 - Show all commits

View File

@ -72,7 +72,7 @@ public class ServiceController : ControllerBase
[HttpPost("update")] [HttpPost("update")]
[SwaggerOperation( [SwaggerOperation(
Summary = "서비스 수정", Summary = "서비스 수정",
Description = "기존 서비스의 정보를 수정합니다. 서비스명, 설명, 웹훅 URL, 태그를 변경할 수 있습니다.")] Description = "기존 서비스의 정보를 수정합니다. 서비스명, 설명, 웹훅 URL, 태그, 상태(Status)를 변경할 수 있습니다. Status 필드(0=Active, 1=Suspended)를 함께 전달하면 상태도 원자적으로 변경됩니다.")]
[SwaggerResponse(200, "수정 성공", typeof(ApiResponse<ServiceResponseDto>))] [SwaggerResponse(200, "수정 성공", typeof(ApiResponse<ServiceResponseDto>))]
[SwaggerResponse(400, "변경된 내용 없음")] [SwaggerResponse(400, "변경된 내용 없음")]
[SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(401, "인증되지 않은 요청")]
@ -192,10 +192,38 @@ public class ServiceController : ControllerBase
return Ok(ApiResponse.Success()); 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<IActionResult> 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<IActionResult> DeleteFcmCredentialsAsync([FromRoute] string serviceCode)
{
await _serviceManagementService.DeleteFcmCredentialsAsync(serviceCode);
return Ok(ApiResponse.Success());
}
[HttpPost("{serviceCode}/credentials")] [HttpPost("{serviceCode}/credentials")]
[SwaggerOperation( [SwaggerOperation(
Summary = "푸시 키 정보 조회", Summary = "푸시 키 정보 조회",
Description = "서비스에 등록된 APNs/FCM 키의 메타 정보를 조회합니다. 민감 정보(Private Key)는 반환되지 않습니다.")] Description = "서비스에 등록된 APNs/FCM 키의 메타 정보를 조회합니다. 민감 정보(Private Key)는 반환되지 않습니다. 각 플랫폼별 credentialStatus(ok/warn/error/none)와 statusReason 필드로 자격증명 상태 진단 결과를 제공합니다.")]
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<CredentialsResponseDto>))] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse<CredentialsResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(401, "인증되지 않은 요청")]
[SwaggerResponse(403, "권한 없음")] [SwaggerResponse(403, "권한 없음")]

View File

@ -17,10 +17,16 @@ public class ApnsCredentialsInfoDto
// p12 메타 // p12 메타
public bool HasCertificate { get; set; } public bool HasCertificate { get; set; }
public DateTime? CertExpiresAt { 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 class FcmCredentialsInfoDto
{ {
public string? ProjectId { get; set; } public string? ProjectId { get; set; }
public bool HasCredentials { get; set; } public bool HasCredentials { get; set; }
// 진단 상태
public string CredentialStatus { get; set; } = "none"; // ok|none
public string? StatusReason { get; set; }
} }

View File

@ -17,4 +17,9 @@ public class UpdateServiceRequestDto
public string? WebhookUrl { get; set; } public string? WebhookUrl { get; set; }
public string? Tags { get; set; } public string? Tags { get; set; }
/// <summary>
/// 서비스 상태 (0: Active, 1: Suspended). 제공 시 상태도 함께 변경됩니다.
/// </summary>
public int? Status { get; set; }
} }

View File

@ -16,6 +16,8 @@ public interface IServiceManagementService
Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request); Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request);
Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request); Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request);
Task<CredentialsResponseDto> GetCredentialsAsync(string serviceCode); Task<CredentialsResponseDto> GetCredentialsAsync(string serviceCode);
Task DeleteApnsCredentialsAsync(string serviceCode);
Task DeleteFcmCredentialsAsync(string serviceCode);
// Tags // Tags
Task<ServiceTagsResponseDto> GetTagsAsync(ServiceTagsRequestDto request); Task<ServiceTagsResponseDto> GetTagsAsync(ServiceTagsRequestDto request);

View File

@ -137,6 +137,17 @@ public class ServiceManagementService : IServiceManagementService
hasChanges = true; hasChanges = true;
} }
// Status 변경
if (request.Status.HasValue)
{
var newStatus = (ServiceStatus)request.Status.Value;
if (service.Status != newStatus)
{
service.Status = newStatus;
hasChanges = true;
}
}
if (!hasChanges) if (!hasChanges)
{ {
throw new SpmsException( throw new SpmsException(
@ -627,7 +638,7 @@ public class ServiceManagementService : IServiceManagementService
// APNs info (meta only, no private key) // APNs info (meta only, no private key)
if (!string.IsNullOrEmpty(service.ApnsBundleId)) if (!string.IsNullOrEmpty(service.ApnsBundleId))
{ {
response.Apns = new ApnsCredentialsInfoDto var apnsInfo = new ApnsCredentialsInfoDto
{ {
BundleId = service.ApnsBundleId, BundleId = service.ApnsBundleId,
AuthType = service.ApnsAuthType, AuthType = service.ApnsAuthType,
@ -637,6 +648,16 @@ public class ServiceManagementService : IServiceManagementService
HasCertificate = !string.IsNullOrEmpty(service.ApnsCertificate), HasCertificate = !string.IsNullOrEmpty(service.ApnsCertificate),
CertExpiresAt = service.ApnsCertExpiresAt 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) // FCM info (project_id only, no private key)
@ -661,13 +682,75 @@ public class ServiceManagementService : IServiceManagementService
response.Fcm = new FcmCredentialsInfoDto response.Fcm = new FcmCredentialsInfoDto
{ {
ProjectId = projectId, ProjectId = projectId,
HasCredentials = true HasCredentials = true,
CredentialStatus = "ok"
}; };
} }
return response; 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<ServiceTagsResponseDto> GetTagsAsync(ServiceTagsRequestDto request) public async Task<ServiceTagsResponseDto> GetTagsAsync(ServiceTagsRequestDto request)
{ {
var service = await _serviceRepository.GetByServiceCodeAsync(request.ServiceCode); var service = await _serviceRepository.GetByServiceCodeAsync(request.ServiceCode);