improvement: 서비스 통합 등록 플로우 구현 (#212) #213
|
|
@ -33,6 +33,23 @@ public class ServiceController : ControllerBase
|
|||
return Ok(ApiResponse<ServiceNameCheckResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[SwaggerOperation(
|
||||
Summary = "서비스 통합 등록",
|
||||
Description = "서비스 생성과 플랫폼 자격증명 등록을 단일 호출로 완료합니다. FCM/APNs 자격증명은 선택사항이며, 제공 시 검증 실패하면 전체 롤백됩니다.")]
|
||||
[SwaggerResponse(200, "등록 성공", typeof(ApiResponse<RegisterServiceResponseDto>))]
|
||||
[SwaggerResponse(400, "잘못된 요청")]
|
||||
[SwaggerResponse(409, "서비스명 중복")]
|
||||
public async Task<IActionResult> RegisterAsync([FromBody] RegisterServiceRequestDto request)
|
||||
{
|
||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||
|
||||
var result = await _serviceManagementService.RegisterAsync(request, adminId);
|
||||
return Ok(ApiResponse<RegisterServiceResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("create")]
|
||||
[SwaggerOperation(
|
||||
Summary = "서비스 등록",
|
||||
|
|
|
|||
46
SPMS.Application/DTOs/Service/RegisterServiceRequestDto.cs
Normal file
46
SPMS.Application/DTOs/Service/RegisterServiceRequestDto.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SPMS.Application.DTOs.Service;
|
||||
|
||||
public class RegisterServiceRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "서비스명은 필수입니다.")]
|
||||
[StringLength(100, MinimumLength = 2, ErrorMessage = "서비스명은 2~100자여야 합니다.")]
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500, ErrorMessage = "설명은 500자 이내여야 합니다.")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// FCM 자격증명 (선택)
|
||||
/// </summary>
|
||||
public FcmCredentialDto? Fcm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// APNs 자격증명 (선택)
|
||||
/// </summary>
|
||||
public ApnsCredentialDto? Apns { get; set; }
|
||||
}
|
||||
|
||||
public class FcmCredentialDto
|
||||
{
|
||||
[Required(ErrorMessage = "Service Account JSON은 필수입니다.")]
|
||||
public string ServiceAccountJson { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ApnsCredentialDto
|
||||
{
|
||||
[Required(ErrorMessage = "Bundle ID는 필수입니다.")]
|
||||
public string BundleId { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Key ID는 필수입니다.")]
|
||||
[StringLength(10, MinimumLength = 10, ErrorMessage = "Key ID는 10자리여야 합니다.")]
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Team ID는 필수입니다.")]
|
||||
[StringLength(10, MinimumLength = 10, ErrorMessage = "Team ID는 10자리여야 합니다.")]
|
||||
public string TeamId { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Private Key는 필수입니다.")]
|
||||
public string PrivateKey { get; set; } = string.Empty;
|
||||
}
|
||||
23
SPMS.Application/DTOs/Service/RegisterServiceResponseDto.cs
Normal file
23
SPMS.Application/DTOs/Service/RegisterServiceResponseDto.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
namespace SPMS.Application.DTOs.Service;
|
||||
|
||||
public class RegisterServiceResponseDto
|
||||
{
|
||||
public string ServiceCode { get; set; } = string.Empty;
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public DateTime ApiKeyCreatedAt { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public PlatformResultDto Platforms { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PlatformResultDto
|
||||
{
|
||||
public PlatformStatusDto? Android { get; set; }
|
||||
public PlatformStatusDto? Ios { get; set; }
|
||||
}
|
||||
|
||||
public class PlatformStatusDto
|
||||
{
|
||||
public bool Registered { get; set; }
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ public interface IServiceManagementService
|
|||
{
|
||||
Task<ServiceNameCheckResponseDto> CheckServiceNameAsync(ServiceNameCheckRequestDto request);
|
||||
Task<CreateServiceResponseDto> CreateAsync(CreateServiceRequestDto request, long adminId);
|
||||
Task<RegisterServiceResponseDto> RegisterAsync(RegisterServiceRequestDto request, long adminId);
|
||||
Task<ServiceResponseDto> UpdateAsync(UpdateServiceRequestDto request);
|
||||
Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request);
|
||||
Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode);
|
||||
|
|
|
|||
|
|
@ -347,14 +347,7 @@ public class ServiceManagementService : IServiceManagementService
|
|||
404);
|
||||
}
|
||||
|
||||
// Validate .p8 private key format
|
||||
if (!request.PrivateKey.Contains("BEGIN PRIVATE KEY") || !request.PrivateKey.Contains("END PRIVATE KEY"))
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.InvalidCredentials,
|
||||
"유효하지 않은 APNs Private Key 형식입니다. .p8 파일 내용을 입력해주세요.",
|
||||
400);
|
||||
}
|
||||
ValidateApnsCredentials(request.PrivateKey);
|
||||
|
||||
// Encrypt and store
|
||||
service.ApnsBundleId = request.BundleId;
|
||||
|
|
@ -378,10 +371,111 @@ public class ServiceManagementService : IServiceManagementService
|
|||
404);
|
||||
}
|
||||
|
||||
// Validate JSON format and required fields
|
||||
ValidateFcmCredentials(request.ServiceAccountJson);
|
||||
|
||||
// Encrypt and store
|
||||
service.FcmCredentials = _credentialEncryptionService.Encrypt(request.ServiceAccountJson);
|
||||
service.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
_serviceRepository.Update(service);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<RegisterServiceResponseDto> RegisterAsync(RegisterServiceRequestDto request, long adminId)
|
||||
{
|
||||
// 1. 서비스명 중복 검사
|
||||
var nameExists = await _serviceRepository.ServiceNameExistsAsync(request.ServiceName);
|
||||
if (nameExists)
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.ServiceNameDuplicate,
|
||||
"이미 존재하는 서비스명입니다.",
|
||||
409);
|
||||
}
|
||||
|
||||
// 2. 자격증명 검증 (트랜잭션 전 빠른 실패)
|
||||
if (request.Fcm != null)
|
||||
ValidateFcmCredentials(request.Fcm.ServiceAccountJson);
|
||||
|
||||
if (request.Apns != null)
|
||||
ValidateApnsCredentials(request.Apns.PrivateKey);
|
||||
|
||||
// 3. 트랜잭션 시작
|
||||
using var transaction = await _unitOfWork.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonDocument.Parse(request.ServiceAccountJson);
|
||||
// 4. 서비스 생성
|
||||
var serviceCode = await GenerateUniqueServiceCodeAsync();
|
||||
|
||||
var randomBytes = new byte[36];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(randomBytes);
|
||||
}
|
||||
var apiKey = Convert.ToBase64String(randomBytes);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var service = new Service
|
||||
{
|
||||
ServiceCode = serviceCode,
|
||||
ServiceName = request.ServiceName,
|
||||
Description = request.Description,
|
||||
ApiKey = apiKey,
|
||||
ApiKeyCreatedAt = now,
|
||||
SubTier = SubTier.Free,
|
||||
Status = ServiceStatus.Active,
|
||||
CreatedAt = now,
|
||||
CreatedBy = adminId,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
// 5. 자격증명 저장 (검증 통과한 것만)
|
||||
if (request.Fcm != null)
|
||||
{
|
||||
service.FcmCredentials = _credentialEncryptionService.Encrypt(request.Fcm.ServiceAccountJson);
|
||||
}
|
||||
|
||||
if (request.Apns != null)
|
||||
{
|
||||
service.ApnsBundleId = request.Apns.BundleId;
|
||||
service.ApnsKeyId = request.Apns.KeyId;
|
||||
service.ApnsTeamId = request.Apns.TeamId;
|
||||
service.ApnsPrivateKey = _credentialEncryptionService.Encrypt(request.Apns.PrivateKey);
|
||||
}
|
||||
|
||||
await _serviceRepository.AddAsync(service);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
await _unitOfWork.CommitTransactionAsync();
|
||||
|
||||
// 6. 응답 반환
|
||||
return new RegisterServiceResponseDto
|
||||
{
|
||||
ServiceCode = service.ServiceCode,
|
||||
ServiceName = service.ServiceName,
|
||||
ApiKey = apiKey,
|
||||
ApiKeyCreatedAt = service.ApiKeyCreatedAt,
|
||||
Status = service.Status.ToString(),
|
||||
CreatedAt = service.CreatedAt,
|
||||
Platforms = new PlatformResultDto
|
||||
{
|
||||
Android = request.Fcm != null ? new PlatformStatusDto { Registered = true } : null,
|
||||
Ios = request.Apns != null ? new PlatformStatusDto { Registered = true } : null
|
||||
}
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackTransactionAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateFcmCredentials(string serviceAccountJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonDocument.Parse(serviceAccountJson);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("project_id", out _) ||
|
||||
|
|
@ -401,13 +495,17 @@ public class ServiceManagementService : IServiceManagementService
|
|||
"유효하지 않은 JSON 형식입니다.",
|
||||
400);
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt and store
|
||||
service.FcmCredentials = _credentialEncryptionService.Encrypt(request.ServiceAccountJson);
|
||||
service.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
_serviceRepository.Update(service);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
private static void ValidateApnsCredentials(string privateKey)
|
||||
{
|
||||
if (!privateKey.Contains("BEGIN PRIVATE KEY") || !privateKey.Contains("END PRIVATE KEY"))
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.InvalidCredentials,
|
||||
"유효하지 않은 APNs Private Key 형식입니다. .p8 파일 내용을 입력해주세요.",
|
||||
400);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CredentialsResponseDto> GetCredentialsAsync(string serviceCode)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user