feat: 서비스 등록 API 구현 (#52) #53
|
|
@ -20,6 +20,27 @@ public class ServiceController : ControllerBase
|
||||||
_serviceManagementService = serviceManagementService;
|
_serviceManagementService = serviceManagementService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("create")]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "서비스 등록",
|
||||||
|
Description = "새로운 서비스를 등록합니다. ServiceCode와 API Key가 자동 생성되며, API Key는 응답에서 1회만 표시됩니다.")]
|
||||||
|
[SwaggerResponse(200, "등록 성공", typeof(ApiResponse<CreateServiceResponseDto>))]
|
||||||
|
[SwaggerResponse(400, "잘못된 요청")]
|
||||||
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||||
|
[SwaggerResponse(403, "권한 없음")]
|
||||||
|
[SwaggerResponse(409, "이미 존재하는 서비스명")]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] CreateServiceRequestDto request)
|
||||||
|
{
|
||||||
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
|
{
|
||||||
|
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _serviceManagementService.CreateAsync(request, adminId);
|
||||||
|
return Ok(ApiResponse<CreateServiceResponseDto>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("list")]
|
[HttpPost("list")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "서비스 목록 조회",
|
Summary = "서비스 목록 조회",
|
||||||
|
|
|
||||||
13
SPMS.Application/DTOs/Service/CreateServiceRequestDto.cs
Normal file
13
SPMS.Application/DTOs/Service/CreateServiceRequestDto.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class CreateServiceRequestDto
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "서비스명은 필수입니다.")]
|
||||||
|
[StringLength(100, ErrorMessage = "서비스명은 100자 이내여야 합니다.")]
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(500, ErrorMessage = "설명은 500자 이내여야 합니다.")]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class CreateServiceResponseDto
|
||||||
|
{
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
public DateTime ApiKeyCreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ namespace SPMS.Application.Interfaces;
|
||||||
|
|
||||||
public interface IServiceManagementService
|
public interface IServiceManagementService
|
||||||
{
|
{
|
||||||
|
Task<CreateServiceResponseDto> CreateAsync(CreateServiceRequestDto request, long adminId);
|
||||||
Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request);
|
Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request);
|
||||||
Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode);
|
Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode);
|
||||||
Task<ServiceResponseDto> ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request);
|
Task<ServiceResponseDto> ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,87 @@ public class ServiceManagementService : IServiceManagementService
|
||||||
_credentialEncryptionService = credentialEncryptionService;
|
_credentialEncryptionService = credentialEncryptionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CreateServiceResponseDto> CreateAsync(CreateServiceRequestDto request, long adminId)
|
||||||
|
{
|
||||||
|
// 서비스명 중복 검사
|
||||||
|
var nameExists = await _serviceRepository.ServiceNameExistsAsync(request.ServiceName);
|
||||||
|
if (nameExists)
|
||||||
|
{
|
||||||
|
throw new SpmsException(
|
||||||
|
ErrorCodes.Conflict,
|
||||||
|
"이미 존재하는 서비스명입니다.",
|
||||||
|
409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceCode 생성 (NanoID 8자리)
|
||||||
|
var serviceCode = await GenerateUniqueServiceCodeAsync();
|
||||||
|
|
||||||
|
// API Key 생성 (48자 = 36바이트 Base64)
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
await _serviceRepository.AddAsync(service);
|
||||||
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new CreateServiceResponseDto
|
||||||
|
{
|
||||||
|
ServiceCode = service.ServiceCode,
|
||||||
|
ApiKey = apiKey,
|
||||||
|
ApiKeyCreatedAt = service.ApiKeyCreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateUniqueServiceCodeAsync()
|
||||||
|
{
|
||||||
|
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
const int codeLength = 8;
|
||||||
|
var buffer = new byte[codeLength];
|
||||||
|
|
||||||
|
for (var attempt = 0; attempt < 10; attempt++)
|
||||||
|
{
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = new char[codeLength];
|
||||||
|
for (var i = 0; i < codeLength; i++)
|
||||||
|
{
|
||||||
|
code[i] = alphabet[buffer[i] % alphabet.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceCode = new string(code);
|
||||||
|
var exists = await _serviceRepository.ServiceCodeExistsAsync(serviceCode);
|
||||||
|
if (!exists)
|
||||||
|
return serviceCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SpmsException(
|
||||||
|
ErrorCodes.InternalError,
|
||||||
|
"서비스 코드 생성에 실패했습니다. 다시 시도해주세요.",
|
||||||
|
500);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request)
|
public async Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request)
|
||||||
{
|
{
|
||||||
Expression<Func<Service, bool>>? predicate = null;
|
Expression<Func<Service, bool>>? predicate = null;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ public interface IServiceRepository : IRepository<Service>
|
||||||
Task<Service?> GetByIdWithIpsAsync(long id);
|
Task<Service?> GetByIdWithIpsAsync(long id);
|
||||||
Task<Service?> GetByServiceCodeWithIpsAsync(string serviceCode);
|
Task<Service?> GetByServiceCodeWithIpsAsync(string serviceCode);
|
||||||
Task<IReadOnlyList<Service>> GetByStatusAsync(ServiceStatus status);
|
Task<IReadOnlyList<Service>> GetByStatusAsync(ServiceStatus status);
|
||||||
|
Task<bool> ServiceNameExistsAsync(string serviceName);
|
||||||
|
Task<bool> ServiceCodeExistsAsync(string serviceCode);
|
||||||
|
|
||||||
// ServiceIp methods
|
// ServiceIp methods
|
||||||
Task<ServiceIp?> GetServiceIpByIdAsync(long ipId);
|
Task<ServiceIp?> GetServiceIpByIdAsync(long ipId);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ public class ServiceRepository : Repository<Service>, IServiceRepository
|
||||||
public async Task<IReadOnlyList<Service>> GetByStatusAsync(ServiceStatus status)
|
public async Task<IReadOnlyList<Service>> GetByStatusAsync(ServiceStatus status)
|
||||||
=> await _dbSet.Where(s => s.Status == status).ToListAsync();
|
=> await _dbSet.Where(s => s.Status == status).ToListAsync();
|
||||||
|
|
||||||
|
public async Task<bool> ServiceNameExistsAsync(string serviceName)
|
||||||
|
=> await _dbSet.AnyAsync(s => s.ServiceName == serviceName);
|
||||||
|
|
||||||
|
public async Task<bool> ServiceCodeExistsAsync(string serviceCode)
|
||||||
|
=> await _dbSet.AnyAsync(s => s.ServiceCode == serviceCode);
|
||||||
|
|
||||||
// ServiceIp methods
|
// ServiceIp methods
|
||||||
public async Task<ServiceIp?> GetServiceIpByIdAsync(long ipId)
|
public async Task<ServiceIp?> GetServiceIpByIdAsync(long ipId)
|
||||||
=> await _context.Set<ServiceIp>().FirstOrDefaultAsync(ip => ip.Id == ipId);
|
=> await _context.Set<ServiceIp>().FirstOrDefaultAsync(ip => ip.Id == ipId);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user