feat: 웹훅 설정 API 구현 (#144) #145
|
|
@ -207,6 +207,22 @@ public class ServiceController : ControllerBase
|
||||||
return Ok(ApiResponse<ServiceTagsResponseDto>.Success(result));
|
return Ok(ApiResponse<ServiceTagsResponseDto>.Success(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("webhook/config")]
|
||||||
|
[SwaggerOperation(Summary = "웹훅 설정", Description = "서비스의 웹훅 URL과 구독 이벤트 타입을 설정합니다.")]
|
||||||
|
public async Task<IActionResult> ConfigureWebhookAsync([FromBody] WebhookConfigRequestDto request)
|
||||||
|
{
|
||||||
|
var result = await _serviceManagementService.ConfigureWebhookAsync(request.ServiceCode, request);
|
||||||
|
return Ok(ApiResponse<WebhookConfigResponseDto>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("webhook/info")]
|
||||||
|
[SwaggerOperation(Summary = "웹훅 설정 조회", Description = "서비스의 현재 웹훅 설정을 조회합니다.")]
|
||||||
|
public async Task<IActionResult> GetWebhookConfigAsync([FromBody] WebhookInfoRequestDto request)
|
||||||
|
{
|
||||||
|
var result = await _serviceManagementService.GetWebhookConfigAsync(request.ServiceCode);
|
||||||
|
return Ok(ApiResponse<WebhookConfigResponseDto>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{serviceCode}/ip/list")]
|
[HttpPost("{serviceCode}/ip/list")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "IP 화이트리스트 조회",
|
Summary = "IP 화이트리스트 조회",
|
||||||
|
|
|
||||||
25
SPMS.Application/DTOs/Service/WebhookConfigRequestDto.cs
Normal file
25
SPMS.Application/DTOs/Service/WebhookConfigRequestDto.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class WebhookConfigRequestDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[JsonPropertyName("service_code")]
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("webhook_url")]
|
||||||
|
[Url]
|
||||||
|
public string? WebhookUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("events")]
|
||||||
|
public List<string>? Events { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebhookInfoRequestDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[JsonPropertyName("service_code")]
|
||||||
|
public string ServiceCode { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
15
SPMS.Application/DTOs/Service/WebhookConfigResponseDto.cs
Normal file
15
SPMS.Application/DTOs/Service/WebhookConfigResponseDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Service;
|
||||||
|
|
||||||
|
public class WebhookConfigResponseDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("webhook_url")]
|
||||||
|
public string? WebhookUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("events")]
|
||||||
|
public List<string> Events { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("is_active")]
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,10 @@ public interface IServiceManagementService
|
||||||
Task<ServiceTagsResponseDto> GetTagsAsync(ServiceTagsRequestDto request);
|
Task<ServiceTagsResponseDto> GetTagsAsync(ServiceTagsRequestDto request);
|
||||||
Task<ServiceTagsResponseDto> UpdateTagsAsync(UpdateServiceTagsRequestDto request);
|
Task<ServiceTagsResponseDto> UpdateTagsAsync(UpdateServiceTagsRequestDto request);
|
||||||
|
|
||||||
|
// Webhook
|
||||||
|
Task<WebhookConfigResponseDto> ConfigureWebhookAsync(string serviceCode, WebhookConfigRequestDto request);
|
||||||
|
Task<WebhookConfigResponseDto> GetWebhookConfigAsync(string serviceCode);
|
||||||
|
|
||||||
// IP Whitelist
|
// IP Whitelist
|
||||||
Task<IpListResponseDto> GetIpListAsync(string serviceCode);
|
Task<IpListResponseDto> GetIpListAsync(string serviceCode);
|
||||||
Task<ServiceIpDto> AddIpAsync(string serviceCode, AddIpRequestDto request);
|
Task<ServiceIpDto> AddIpAsync(string serviceCode, AddIpRequestDto request);
|
||||||
|
|
|
||||||
|
|
@ -524,6 +524,60 @@ public class ServiceManagementService : IServiceManagementService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WebhookConfigResponseDto> ConfigureWebhookAsync(string serviceCode, WebhookConfigRequestDto request)
|
||||||
|
{
|
||||||
|
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||||
|
if (service is null)
|
||||||
|
throw new SpmsException(ErrorCodes.NotFound, "서비스를 찾을 수 없습니다.", 404);
|
||||||
|
|
||||||
|
if (request.Events != null)
|
||||||
|
{
|
||||||
|
var validEvents = new HashSet<string> { "push_sent", "push_failed", "push_clicked" };
|
||||||
|
foreach (var evt in request.Events)
|
||||||
|
{
|
||||||
|
if (!validEvents.Contains(evt))
|
||||||
|
throw new SpmsException(ErrorCodes.BadRequest, $"유효하지 않은 이벤트 타입: {evt}", 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.WebhookUrl = request.WebhookUrl;
|
||||||
|
service.WebhookEvents = request.Events != null && request.Events.Count > 0
|
||||||
|
? JsonSerializer.Serialize(request.Events)
|
||||||
|
: null;
|
||||||
|
service.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_serviceRepository.Update(service);
|
||||||
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
|
return BuildWebhookConfigResponse(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WebhookConfigResponseDto> GetWebhookConfigAsync(string serviceCode)
|
||||||
|
{
|
||||||
|
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
|
||||||
|
if (service is null)
|
||||||
|
throw new SpmsException(ErrorCodes.NotFound, "서비스를 찾을 수 없습니다.", 404);
|
||||||
|
|
||||||
|
return BuildWebhookConfigResponse(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebhookConfigResponseDto BuildWebhookConfigResponse(Service service)
|
||||||
|
{
|
||||||
|
var events = new List<string>();
|
||||||
|
if (!string.IsNullOrEmpty(service.WebhookEvents))
|
||||||
|
{
|
||||||
|
try { events = JsonSerializer.Deserialize<List<string>>(service.WebhookEvents) ?? new(); }
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WebhookConfigResponseDto
|
||||||
|
{
|
||||||
|
WebhookUrl = service.WebhookUrl,
|
||||||
|
Events = events,
|
||||||
|
IsActive = !string.IsNullOrEmpty(service.WebhookUrl) && events.Count > 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IpListResponseDto> GetIpListAsync(string serviceCode)
|
public async Task<IpListResponseDto> GetIpListAsync(string serviceCode)
|
||||||
{
|
{
|
||||||
var service = await _serviceRepository.GetByServiceCodeWithIpsAsync(serviceCode);
|
var service = await _serviceRepository.GetByServiceCodeWithIpsAsync(serviceCode);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public class Service : BaseEntity
|
||||||
public string? ApnsPrivateKey { get; set; }
|
public string? ApnsPrivateKey { get; set; }
|
||||||
public string? FcmCredentials { get; set; }
|
public string? FcmCredentials { get; set; }
|
||||||
public string? WebhookUrl { get; set; }
|
public string? WebhookUrl { get; set; }
|
||||||
|
public string? WebhookEvents { get; set; }
|
||||||
public string? Tags { get; set; }
|
public string? Tags { get; set; }
|
||||||
public SubTier SubTier { get; set; }
|
public SubTier SubTier { get; set; }
|
||||||
public DateTime? SubStartedAt { get; set; }
|
public DateTime? SubStartedAt { get; set; }
|
||||||
|
|
|
||||||
1102
SPMS.Infrastructure/Migrations/20260211010030_AddWebhookEventsToService.Designer.cs
generated
Normal file
1102
SPMS.Infrastructure/Migrations/20260211010030_AddWebhookEventsToService.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,29 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWebhookEventsToService : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WebhookEvents",
|
||||||
|
table: "Service",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WebhookEvents",
|
||||||
|
table: "Service");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -721,6 +721,9 @@ namespace SPMS.Infrastructure.Migrations
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("WebhookEvents")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<string>("WebhookUrl")
|
b.Property<string>("WebhookUrl")
|
||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("varchar(500)");
|
.HasColumnType("varchar(500)");
|
||||||
|
|
|
||||||
4
TASKS.md
4
TASKS.md
|
|
@ -1652,8 +1652,8 @@ Milestone: Phase 3: 메시지 & Push Core
|
||||||
| 6 | [Feature] 발송 상세 로그 조회 API | Feature | High | DDL-02 | ✅ |
|
| 6 | [Feature] 발송 상세 로그 조회 API | Feature | High | DDL-02 | ✅ |
|
||||||
| 7 | [Feature] 통계 리포트 다운로드 API | Feature | Medium | EXP-01 | ✅ |
|
| 7 | [Feature] 통계 리포트 다운로드 API | Feature | Medium | EXP-01 | ✅ |
|
||||||
| 8 | [Feature] 상세 로그 다운로드 API | Feature | Medium | EXP-02 | ✅ |
|
| 8 | [Feature] 상세 로그 다운로드 API | Feature | Medium | EXP-02 | ✅ |
|
||||||
| 9 | [Feature] 실패원인 순위 API | Feature | Medium | ANA-01 | ⬜ |
|
| 9 | [Feature] 실패원인 순위 API | Feature | Medium | ANA-01 | ✅ |
|
||||||
| 10 | [Feature] 웹훅 설정 API | Feature | High | WHK-01 | ⬜ |
|
| 10 | [Feature] 웹훅 설정 API | Feature | High | WHK-01 | ✅ |
|
||||||
| 11 | [Feature] **웹훅 발송 서비스** | Feature | High | WHK-02 | ⬜ |
|
| 11 | [Feature] **웹훅 발송 서비스** | Feature | High | WHK-02 | ⬜ |
|
||||||
| 12 | [Feature] **DailyStatWorker 구현** | Feature | Medium | AAG-01 | ⬜ |
|
| 12 | [Feature] **DailyStatWorker 구현** | Feature | Medium | AAG-01 | ⬜ |
|
||||||
| 13 | [Feature] **DeadTokenCleanupWorker 구현** | Feature | Medium | DTK-01 | ⬜ |
|
| 13 | [Feature] **DeadTokenCleanupWorker 구현** | Feature | Medium | DTK-01 | ⬜ |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user