feat: 서비스 관리 API 구현 (#44)
- ServiceController: 서비스 목록/상세/상태변경 엔드포인트 - ServiceManagementService: 비즈니스 로직 구현 - Service DTOs: 요청/응답 DTO 4종 - DI 등록 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b58662b520
commit
cac56761f4
66
SPMS.API/Controllers/ServiceController.cs
Normal file
66
SPMS.API/Controllers/ServiceController.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using SPMS.Application.DTOs.Service;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
|
||||||
|
namespace SPMS.API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("v1/in/service")]
|
||||||
|
[ApiExplorerSettings(GroupName = "service")]
|
||||||
|
[Authorize(Roles = "Super")]
|
||||||
|
public class ServiceController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IServiceManagementService _serviceManagementService;
|
||||||
|
|
||||||
|
public ServiceController(IServiceManagementService serviceManagementService)
|
||||||
|
{
|
||||||
|
_serviceManagementService = serviceManagementService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("list")]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "서비스 목록 조회",
|
||||||
|
Description = "등록된 서비스 목록을 조회합니다. 페이징, 검색, 상태 필터를 지원합니다.")]
|
||||||
|
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceListResponseDto>))]
|
||||||
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||||
|
[SwaggerResponse(403, "권한 없음")]
|
||||||
|
public async Task<IActionResult> GetListAsync([FromBody] ServiceListRequestDto request)
|
||||||
|
{
|
||||||
|
var result = await _serviceManagementService.GetListAsync(request);
|
||||||
|
return Ok(ApiResponse<ServiceListResponseDto>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serviceCode}")]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "서비스 상세 조회",
|
||||||
|
Description = "특정 서비스의 상세 정보를 조회합니다.")]
|
||||||
|
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceResponseDto>))]
|
||||||
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||||
|
[SwaggerResponse(403, "권한 없음")]
|
||||||
|
[SwaggerResponse(404, "서비스를 찾을 수 없음")]
|
||||||
|
public async Task<IActionResult> GetByServiceCodeAsync([FromRoute] string serviceCode)
|
||||||
|
{
|
||||||
|
var result = await _serviceManagementService.GetByServiceCodeAsync(serviceCode);
|
||||||
|
return Ok(ApiResponse<ServiceResponseDto>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serviceCode}/status")]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "서비스 상태 변경",
|
||||||
|
Description = "서비스의 상태를 변경합니다. (Active: 0, Suspended: 1)")]
|
||||||
|
[SwaggerResponse(200, "상태 변경 성공", typeof(ApiResponse<ServiceResponseDto>))]
|
||||||
|
[SwaggerResponse(400, "잘못된 요청 또는 이미 해당 상태")]
|
||||||
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||||
|
[SwaggerResponse(403, "권한 없음")]
|
||||||
|
[SwaggerResponse(404, "서비스를 찾을 수 없음")]
|
||||||
|
public async Task<IActionResult> ChangeStatusAsync(
|
||||||
|
[FromRoute] string serviceCode,
|
||||||
|
[FromBody] ChangeServiceStatusRequestDto request)
|
||||||
|
{
|
||||||
|
var result = await _serviceManagementService.ChangeStatusAsync(serviceCode, request);
|
||||||
|
return Ok(ApiResponse<ServiceResponseDto>.Success(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class ChangeServiceStatusRequestDto
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "상태를 선택해주세요.")]
|
||||||
|
[Range(0, 1, ErrorMessage = "상태는 Active(0) 또는 Suspended(1)만 가능합니다.")]
|
||||||
|
public int Status { get; set; }
|
||||||
|
}
|
||||||
15
SPMS.Application/DTOs/Service/ServiceListRequestDto.cs
Normal file
15
SPMS.Application/DTOs/Service/ServiceListRequestDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class ServiceListRequestDto
|
||||||
|
{
|
||||||
|
[Range(1, int.MaxValue, ErrorMessage = "페이지 번호는 1 이상이어야 합니다.")]
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
[Range(1, 100, ErrorMessage = "페이지 크기는 1~100 사이여야 합니다.")]
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
|
||||||
|
public string? SearchKeyword { get; set; }
|
||||||
|
public int? Status { get; set; }
|
||||||
|
}
|
||||||
21
SPMS.Application/DTOs/Service/ServiceListResponseDto.cs
Normal file
21
SPMS.Application/DTOs/Service/ServiceListResponseDto.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class ServiceListResponseDto
|
||||||
|
{
|
||||||
|
public List<ServiceSummaryDto> Items { get; set; } = new();
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServiceSummaryDto
|
||||||
|
{
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string SubTier { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public int DeviceCount { get; set; }
|
||||||
|
}
|
||||||
25
SPMS.Application/DTOs/Service/ServiceResponseDto.cs
Normal file
25
SPMS.Application/DTOs/Service/ServiceResponseDto.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class ServiceResponseDto
|
||||||
|
{
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
public DateTime ApiKeyCreatedAt { get; set; }
|
||||||
|
public string? ApnsBundleId { get; set; }
|
||||||
|
public string? ApnsKeyId { get; set; }
|
||||||
|
public string? ApnsTeamId { get; set; }
|
||||||
|
public bool HasApnsKey { get; set; }
|
||||||
|
public bool HasFcmCredentials { get; set; }
|
||||||
|
public string? WebhookUrl { get; set; }
|
||||||
|
public string? Tags { get; set; }
|
||||||
|
public string SubTier { get; set; } = string.Empty;
|
||||||
|
public DateTime? SubStartedAt { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string CreatedByName { get; set; } = string.Empty;
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public int DeviceCount { get; set; }
|
||||||
|
public List<string> AllowedIps { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ public static class DependencyInjection
|
||||||
// Application Services
|
// Application Services
|
||||||
services.AddScoped<IAuthService, AuthService>();
|
services.AddScoped<IAuthService, AuthService>();
|
||||||
services.AddScoped<IAccountService, AccountService>();
|
services.AddScoped<IAccountService, AccountService>();
|
||||||
|
services.AddScoped<IServiceManagementService, ServiceManagementService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
SPMS.Application/Interfaces/IServiceManagementService.cs
Normal file
10
SPMS.Application/Interfaces/IServiceManagementService.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
using SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
namespace SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IServiceManagementService
|
||||||
|
{
|
||||||
|
Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request);
|
||||||
|
Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode);
|
||||||
|
Task<ServiceResponseDto> ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request);
|
||||||
|
}
|
||||||
153
SPMS.Application/Services/ServiceManagementService.cs
Normal file
153
SPMS.Application/Services/ServiceManagementService.cs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using SPMS.Application.DTOs.Service;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
using SPMS.Domain.Entities;
|
||||||
|
using SPMS.Domain.Enums;
|
||||||
|
using SPMS.Domain.Exceptions;
|
||||||
|
using SPMS.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.Application.Services;
|
||||||
|
|
||||||
|
public class ServiceManagementService : IServiceManagementService
|
||||||
|
{
|
||||||
|
private readonly IServiceRepository _serviceRepository;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public ServiceManagementService(IServiceRepository serviceRepository, IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_serviceRepository = serviceRepository;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceListResponseDto> GetListAsync(ServiceListRequestDto request)
|
||||||
|
{
|
||||||
|
Expression<Func<Service, bool>>? predicate = null;
|
||||||
|
|
||||||
|
// 검색어 필터
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.SearchKeyword))
|
||||||
|
{
|
||||||
|
var keyword = request.SearchKeyword;
|
||||||
|
predicate = s => s.ServiceName.Contains(keyword) || s.ServiceCode.Contains(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status 필터
|
||||||
|
if (request.Status.HasValue)
|
||||||
|
{
|
||||||
|
var status = (ServiceStatus)request.Status.Value;
|
||||||
|
if (predicate != null)
|
||||||
|
{
|
||||||
|
var basePredicate = predicate;
|
||||||
|
predicate = s => basePredicate.Compile()(s) && s.Status == status;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
predicate = s => s.Status == status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (items, totalCount) = await _serviceRepository.GetPagedAsync(
|
||||||
|
request.Page,
|
||||||
|
request.PageSize,
|
||||||
|
predicate,
|
||||||
|
s => s.CreatedAt,
|
||||||
|
descending: true);
|
||||||
|
|
||||||
|
return new ServiceListResponseDto
|
||||||
|
{
|
||||||
|
Items = items.Select(MapToSummaryDto).ToList(),
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode)
|
||||||
|
{
|
||||||
|
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||||
|
if (service is null)
|
||||||
|
{
|
||||||
|
throw new SpmsException(
|
||||||
|
ErrorCodes.NotFound,
|
||||||
|
"서비스를 찾을 수 없습니다.",
|
||||||
|
404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP 목록 포함해서 다시 조회
|
||||||
|
var serviceWithIps = await _serviceRepository.GetByIdWithIpsAsync(service.Id);
|
||||||
|
|
||||||
|
return MapToDto(serviceWithIps ?? service);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponseDto> ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request)
|
||||||
|
{
|
||||||
|
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||||
|
if (service is null)
|
||||||
|
{
|
||||||
|
throw new SpmsException(
|
||||||
|
ErrorCodes.NotFound,
|
||||||
|
"서비스를 찾을 수 없습니다.",
|
||||||
|
404);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newStatus = (ServiceStatus)request.Status;
|
||||||
|
|
||||||
|
// 이미 같은 상태면 변경 없음
|
||||||
|
if (service.Status == newStatus)
|
||||||
|
{
|
||||||
|
throw new SpmsException(
|
||||||
|
ErrorCodes.NoChange,
|
||||||
|
"이미 해당 상태입니다.",
|
||||||
|
400);
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Status = newStatus;
|
||||||
|
service.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_serviceRepository.Update(service);
|
||||||
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
|
return MapToDto(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceSummaryDto MapToSummaryDto(Service service)
|
||||||
|
{
|
||||||
|
return new ServiceSummaryDto
|
||||||
|
{
|
||||||
|
ServiceCode = service.ServiceCode,
|
||||||
|
ServiceName = service.ServiceName,
|
||||||
|
Description = service.Description,
|
||||||
|
SubTier = service.SubTier.ToString(),
|
||||||
|
Status = service.Status.ToString(),
|
||||||
|
CreatedAt = service.CreatedAt,
|
||||||
|
DeviceCount = service.Devices?.Count ?? 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceResponseDto MapToDto(Service service)
|
||||||
|
{
|
||||||
|
return new ServiceResponseDto
|
||||||
|
{
|
||||||
|
ServiceCode = service.ServiceCode,
|
||||||
|
ServiceName = service.ServiceName,
|
||||||
|
Description = service.Description,
|
||||||
|
ApiKey = service.ApiKey,
|
||||||
|
ApiKeyCreatedAt = service.ApiKeyCreatedAt,
|
||||||
|
ApnsBundleId = service.ApnsBundleId,
|
||||||
|
ApnsKeyId = service.ApnsKeyId,
|
||||||
|
ApnsTeamId = service.ApnsTeamId,
|
||||||
|
HasApnsKey = !string.IsNullOrEmpty(service.ApnsPrivateKey),
|
||||||
|
HasFcmCredentials = !string.IsNullOrEmpty(service.FcmCredentials),
|
||||||
|
WebhookUrl = service.WebhookUrl,
|
||||||
|
Tags = service.Tags,
|
||||||
|
SubTier = service.SubTier.ToString(),
|
||||||
|
SubStartedAt = service.SubStartedAt,
|
||||||
|
Status = service.Status.ToString(),
|
||||||
|
CreatedAt = service.CreatedAt,
|
||||||
|
CreatedByName = service.CreatedByAdmin?.Name ?? string.Empty,
|
||||||
|
UpdatedAt = service.UpdatedAt,
|
||||||
|
DeviceCount = service.Devices?.Count ?? 0,
|
||||||
|
AllowedIps = service.ServiceIps?.Select(ip => ip.IpAddress).ToList() ?? new List<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user