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; using SPMS.Domain.Exceptions; 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("name/check")] [SwaggerOperation( Summary = "서비스명 중복 체크", Description = "서비스 등록 전 서비스명 사용 가능 여부를 확인합니다.")] [SwaggerResponse(200, "중복 체크 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청")] public async Task CheckServiceNameAsync([FromBody] ServiceNameCheckRequestDto request) { var result = await _serviceManagementService.CheckServiceNameAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("register")] [SwaggerOperation( Summary = "서비스 통합 등록", Description = "서비스 생성과 플랫폼 자격증명 등록을 단일 호출로 완료합니다. FCM/APNs 자격증명은 선택사항이며, 제공 시 검증 실패하면 전체 롤백됩니다. APNs는 AuthType(p8/p12)에 따라 필수 필드가 달라집니다.")] [SwaggerResponse(200, "등록 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청")] [SwaggerResponse(409, "서비스명 중복")] public async Task 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.Success(result)); } [HttpPost("create")] [SwaggerOperation( Summary = "서비스 등록", Description = "새로운 서비스를 등록합니다. ServiceCode와 API Key가 자동 생성되며, API Key는 응답에서 1회만 표시됩니다.")] [SwaggerResponse(200, "등록 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(409, "이미 존재하는 서비스명")] public async Task CreateAsync([FromBody] CreateServiceRequestDto request) { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); var result = await _serviceManagementService.CreateAsync(request, adminId); return Ok(ApiResponse.Success(result)); } [HttpPost("update")] [SwaggerOperation( Summary = "서비스 수정", Description = "기존 서비스의 정보를 수정합니다. 서비스명, 설명, 웹훅 URL, 태그를 변경할 수 있습니다.")] [SwaggerResponse(200, "수정 성공", typeof(ApiResponse))] [SwaggerResponse(400, "변경된 내용 없음")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] [SwaggerResponse(409, "이미 존재하는 서비스명")] public async Task UpdateAsync([FromBody] UpdateServiceRequestDto request) { var result = await _serviceManagementService.UpdateAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("delete")] [SwaggerOperation( Summary = "서비스 삭제", Description = "서비스를 Soft Delete 처리합니다. IsDeleted=true, 상태를 Suspended로 변경합니다.")] [SwaggerResponse(200, "삭제 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] [SwaggerResponse(409, "이미 삭제된 서비스")] public async Task DeleteAsync([FromBody] DeleteServiceRequestDto request) { await _serviceManagementService.DeleteAsync(request); return Ok(ApiResponse.Success()); } [HttpPost("list")] [SwaggerOperation( Summary = "서비스 목록 조회", Description = "등록된 서비스 목록을 조회합니다. 페이징, 검색, 상태 필터를 지원합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] public async Task GetListAsync([FromBody] ServiceListRequestDto request) { var result = await _serviceManagementService.GetListAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}")] [SwaggerOperation( Summary = "서비스 상세 조회", Description = "특정 서비스의 상세 정보를 조회합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task GetByServiceCodeAsync([FromRoute] string serviceCode) { var result = await _serviceManagementService.GetByServiceCodeAsync(serviceCode); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}/status")] [SwaggerOperation( Summary = "서비스 상태 변경", Description = "서비스의 상태를 변경합니다. (Active: 0, Suspended: 1)")] [SwaggerResponse(200, "상태 변경 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청 또는 이미 해당 상태")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task ChangeStatusAsync( [FromRoute] string serviceCode, [FromBody] ChangeServiceStatusRequestDto request) { var result = await _serviceManagementService.ChangeStatusAsync(serviceCode, request); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}/apikey/refresh")] [SwaggerOperation( Summary = "API Key 재발급", Description = "서비스의 API Key를 재발급합니다. 기존 키는 즉시 무효화되며, 새 키는 1회만 표시됩니다.")] [SwaggerResponse(200, "재발급 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task RefreshApiKeyAsync([FromRoute] string serviceCode) { var result = await _serviceManagementService.RefreshApiKeyAsync(serviceCode); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}/apns")] [SwaggerOperation( Summary = "APNs 키 등록", Description = "APNs 푸시 발송을 위한 인증 정보를 등록합니다. AuthType=p8: Key ID(10자리), Team ID(10자리), Private Key(.p8) 필수. AuthType=p12: CertificateBase64(Base64 인코딩된 .p12), CertPassword 필수. 타입 전환 시 이전 타입 필드는 자동 초기화됩니다.")] [SwaggerResponse(200, "등록 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청 (유효하지 않은 키/인증서 형식)")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task RegisterApnsCredentialsAsync( [FromRoute] string serviceCode, [FromBody] ApnsCredentialsRequestDto request) { await _serviceManagementService.RegisterApnsCredentialsAsync(serviceCode, request); return Ok(ApiResponse.Success()); } [HttpPost("{serviceCode}/fcm")] [SwaggerOperation( Summary = "FCM 키 등록", Description = "FCM 푸시 발송을 위한 Service Account JSON을 등록합니다. Firebase Console에서 다운로드한 service-account.json 내용이 필요합니다.")] [SwaggerResponse(200, "등록 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청 (유효하지 않은 JSON 형식)")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task RegisterFcmCredentialsAsync( [FromRoute] string serviceCode, [FromBody] FcmCredentialsRequestDto request) { await _serviceManagementService.RegisterFcmCredentialsAsync(serviceCode, request); return Ok(ApiResponse.Success()); } [HttpPost("{serviceCode}/credentials")] [SwaggerOperation( Summary = "푸시 키 정보 조회", Description = "서비스에 등록된 APNs/FCM 키의 메타 정보를 조회합니다. 민감 정보(Private Key)는 반환되지 않습니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task GetCredentialsAsync([FromRoute] string serviceCode) { var result = await _serviceManagementService.GetCredentialsAsync(serviceCode); return Ok(ApiResponse.Success(result)); } [HttpPost("tags/list")] [SwaggerOperation( Summary = "태그 목록 조회", Description = "서비스에 등록된 태그 목록을 조회합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task GetTagsAsync([FromBody] ServiceTagsRequestDto request) { var result = await _serviceManagementService.GetTagsAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("tags/update")] [SwaggerOperation( Summary = "태그 수정", Description = "서비스의 태그 목록을 수정합니다. 최대 10개까지 등록 가능합니다.")] [SwaggerResponse(200, "수정 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청 또는 변경 없음")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task UpdateTagsAsync([FromBody] UpdateServiceTagsRequestDto request) { var result = await _serviceManagementService.UpdateTagsAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("webhook/config")] [SwaggerOperation(Summary = "웹훅 설정", Description = "서비스의 웹훅 URL과 구독 이벤트 타입을 설정합니다.")] public async Task ConfigureWebhookAsync([FromBody] WebhookConfigRequestDto request) { var result = await _serviceManagementService.ConfigureWebhookAsync(request.ServiceCode, request); return Ok(ApiResponse.Success(result)); } [HttpPost("webhook/info")] [SwaggerOperation(Summary = "웹훅 설정 조회", Description = "서비스의 현재 웹훅 설정을 조회합니다.")] public async Task GetWebhookConfigAsync([FromBody] WebhookInfoRequestDto request) { var result = await _serviceManagementService.GetWebhookConfigAsync(request.ServiceCode); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}/ip/list")] [SwaggerOperation( Summary = "IP 화이트리스트 조회", Description = "서비스에 등록된 IP 화이트리스트 목록을 조회합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] public async Task GetIpListAsync([FromRoute] string serviceCode) { var result = await _serviceManagementService.GetIpListAsync(serviceCode); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}/ip/add")] [SwaggerOperation( Summary = "IP 추가", Description = "서비스의 IP 화이트리스트에 새 IP를 추가합니다. IPv4 형식만 지원합니다.")] [SwaggerResponse(200, "추가 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청 (유효하지 않은 IP 형식)")] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스를 찾을 수 없음")] [SwaggerResponse(409, "이미 등록된 IP")] public async Task AddIpAsync( [FromRoute] string serviceCode, [FromBody] AddIpRequestDto request) { var result = await _serviceManagementService.AddIpAsync(serviceCode, request); return Ok(ApiResponse.Success(result)); } [HttpPost("{serviceCode}/ip/delete")] [SwaggerOperation( Summary = "IP 삭제", Description = "서비스의 IP 화이트리스트에서 IP를 삭제합니다.")] [SwaggerResponse(200, "삭제 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] [SwaggerResponse(404, "서비스 또는 IP를 찾을 수 없음")] public async Task DeleteIpAsync( [FromRoute] string serviceCode, [FromBody] DeleteIpRequestDto request) { await _serviceManagementService.DeleteIpAsync(serviceCode, request); return Ok(ApiResponse.Success()); } }