Compare commits
4 Commits
4f806ecdb1
...
3fc3bb8144
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fc3bb8144 | |||
| efd6615809 | |||
| 355d3269c0 | |||
|
|
b56170f10c |
|
|
@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Infrastructure;
|
||||
using SPMS.Infrastructure.Messaging;
|
||||
|
||||
namespace SPMS.API.Controllers;
|
||||
|
||||
|
|
@ -14,10 +15,17 @@ namespace SPMS.API.Controllers;
|
|||
public class PublicController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _dbContext;
|
||||
private readonly RabbitMQConnection _rabbitConnection;
|
||||
private readonly RabbitMQInitializer _rabbitInitializer;
|
||||
|
||||
public PublicController(AppDbContext dbContext)
|
||||
public PublicController(
|
||||
AppDbContext dbContext,
|
||||
RabbitMQConnection rabbitConnection,
|
||||
RabbitMQInitializer rabbitInitializer)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_rabbitConnection = rabbitConnection;
|
||||
_rabbitInitializer = rabbitInitializer;
|
||||
}
|
||||
|
||||
[HttpPost("health")]
|
||||
|
|
@ -39,11 +47,26 @@ public class PublicController : ControllerBase
|
|||
allHealthy = false;
|
||||
}
|
||||
|
||||
// 2. Redis 연결 확인 (Phase 2에서 구현 예정)
|
||||
// 2. Redis 연결 확인 (Phase 3-2에서 구현 예정)
|
||||
checks["redis"] = new { status = "not_configured" };
|
||||
|
||||
// 3. RabbitMQ 연결 확인 (Phase 2에서 구현 예정)
|
||||
checks["rabbitmq"] = new { status = "not_configured" };
|
||||
// 3. RabbitMQ 연결 확인
|
||||
var rabbitConnected = _rabbitConnection.IsConnected;
|
||||
var rabbitInitialized = _rabbitInitializer.IsInitialized;
|
||||
if (rabbitConnected && rabbitInitialized)
|
||||
{
|
||||
checks["rabbitmq"] = new { status = "healthy" };
|
||||
}
|
||||
else
|
||||
{
|
||||
checks["rabbitmq"] = new
|
||||
{
|
||||
status = "unhealthy",
|
||||
connected = rabbitConnected,
|
||||
initialized = rabbitInitialized
|
||||
};
|
||||
allHealthy = false;
|
||||
}
|
||||
|
||||
if (allHealthy)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ public class PushController : ControllerBase
|
|||
return Ok(ApiResponse.Success());
|
||||
}
|
||||
|
||||
[HttpPost("log")]
|
||||
[SwaggerOperation(Summary = "발송 로그 조회", Description = "푸시 발송 이력을 페이지 단위로 조회합니다.")]
|
||||
public async Task<IActionResult> GetLogAsync([FromBody] PushLogRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var result = await _pushService.GetLogAsync(serviceId, request);
|
||||
return Ok(ApiResponse<PushLogResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
private long GetServiceId()
|
||||
{
|
||||
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
|
||||
|
|
|
|||
27
SPMS.Application/DTOs/Push/PushLogRequestDto.cs
Normal file
27
SPMS.Application/DTOs/Push/PushLogRequestDto.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Push;
|
||||
|
||||
public class PushLogRequestDto
|
||||
{
|
||||
[JsonPropertyName("page")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int Size { get; set; } = 20;
|
||||
|
||||
[JsonPropertyName("message_code")]
|
||||
public string? MessageCode { get; set; }
|
||||
|
||||
[JsonPropertyName("device_id")]
|
||||
public long? DeviceId { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("start_date")]
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("end_date")]
|
||||
public string? EndDate { get; set; }
|
||||
}
|
||||
34
SPMS.Application/DTOs/Push/PushLogResponseDto.cs
Normal file
34
SPMS.Application/DTOs/Push/PushLogResponseDto.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using SPMS.Application.DTOs.Notice;
|
||||
|
||||
namespace SPMS.Application.DTOs.Push;
|
||||
|
||||
public class PushLogResponseDto
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public List<PushLogItemDto> Items { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("pagination")]
|
||||
public PaginationDto Pagination { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PushLogItemDto
|
||||
{
|
||||
[JsonPropertyName("send_id")]
|
||||
public long SendId { get; set; }
|
||||
|
||||
[JsonPropertyName("message_code")]
|
||||
public string MessageCode { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("device_id")]
|
||||
public long DeviceId { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("fail_reason")]
|
||||
public string? FailReason { get; set; }
|
||||
|
||||
[JsonPropertyName("sent_at")]
|
||||
public DateTime SentAt { get; set; }
|
||||
}
|
||||
14
SPMS.Application/DTOs/Push/PushSendRequestDto.cs
Normal file
14
SPMS.Application/DTOs/Push/PushSendRequestDto.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SPMS.Application.DTOs.Push;
|
||||
|
||||
public class PushSendRequestDto
|
||||
{
|
||||
[Required]
|
||||
public string MessageCode { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public long DeviceId { get; set; }
|
||||
|
||||
public Dictionary<string, string>? Variables { get; set; }
|
||||
}
|
||||
8
SPMS.Application/DTOs/Push/PushSendResponseDto.cs
Normal file
8
SPMS.Application/DTOs/Push/PushSendResponseDto.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace SPMS.Application.DTOs.Push;
|
||||
|
||||
public class PushSendResponseDto
|
||||
{
|
||||
public string RequestId { get; set; } = string.Empty;
|
||||
public string SendType { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = "queued";
|
||||
}
|
||||
15
SPMS.Application/DTOs/Push/PushSendTagRequestDto.cs
Normal file
15
SPMS.Application/DTOs/Push/PushSendTagRequestDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SPMS.Application.DTOs.Push;
|
||||
|
||||
public class PushSendTagRequestDto
|
||||
{
|
||||
[Required]
|
||||
public string MessageCode { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public List<int> Tags { get; set; } = [];
|
||||
|
||||
public string TagMatch { get; set; } = "or";
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ public interface IPushService
|
|||
Task<PushSendResponseDto> SendByTagAsync(long serviceId, PushSendTagRequestDto request);
|
||||
Task<PushScheduleResponseDto> ScheduleAsync(long serviceId, PushScheduleRequestDto request);
|
||||
Task CancelScheduleAsync(PushScheduleCancelRequestDto request);
|
||||
Task<PushLogResponseDto> GetLogAsync(long serviceId, PushLogRequestDto request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using SPMS.Application.DTOs.Notice;
|
||||
using SPMS.Application.DTOs.Push;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Domain.Enums;
|
||||
using SPMS.Domain.Exceptions;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
|
|
@ -12,15 +15,18 @@ public class PushService : IPushService
|
|||
private readonly IMessageRepository _messageRepository;
|
||||
private readonly IPushQueueService _pushQueueService;
|
||||
private readonly IScheduleCancelStore _scheduleCancelStore;
|
||||
private readonly IPushSendLogRepository _pushSendLogRepository;
|
||||
|
||||
public PushService(
|
||||
IMessageRepository messageRepository,
|
||||
IPushQueueService pushQueueService,
|
||||
IScheduleCancelStore scheduleCancelStore)
|
||||
IScheduleCancelStore scheduleCancelStore,
|
||||
IPushSendLogRepository pushSendLogRepository)
|
||||
{
|
||||
_messageRepository = messageRepository;
|
||||
_pushQueueService = pushQueueService;
|
||||
_scheduleCancelStore = scheduleCancelStore;
|
||||
_pushSendLogRepository = pushSendLogRepository;
|
||||
}
|
||||
|
||||
public async Task<PushSendResponseDto> SendAsync(long serviceId, PushSendRequestDto request)
|
||||
|
|
@ -186,6 +192,77 @@ public class PushService : IPushService
|
|||
await _scheduleCancelStore.MarkCancelledAsync(request.ScheduleId);
|
||||
}
|
||||
|
||||
public async Task<PushLogResponseDto> GetLogAsync(long serviceId, PushLogRequestDto request)
|
||||
{
|
||||
long? messageId = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.MessageCode))
|
||||
{
|
||||
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
||||
if (message != null)
|
||||
messageId = message.Id;
|
||||
else
|
||||
return new PushLogResponseDto
|
||||
{
|
||||
Items = [],
|
||||
Pagination = new PaginationDto
|
||||
{
|
||||
Page = request.Page,
|
||||
Size = request.Size,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
PushResult? status = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.Status))
|
||||
{
|
||||
status = request.Status.ToLowerInvariant() switch
|
||||
{
|
||||
"success" => PushResult.Success,
|
||||
"failed" => PushResult.Failed,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
DateTime? startDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.StartDate) &&
|
||||
DateTime.TryParseExact(request.StartDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedStart))
|
||||
startDate = parsedStart;
|
||||
|
||||
DateTime? endDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.EndDate) &&
|
||||
DateTime.TryParseExact(request.EndDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedEnd))
|
||||
endDate = parsedEnd;
|
||||
|
||||
var (items, totalCount) = await _pushSendLogRepository.GetPagedWithMessageAsync(
|
||||
serviceId, request.Page, request.Size,
|
||||
messageId, request.DeviceId, status,
|
||||
startDate, endDate);
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
||||
|
||||
return new PushLogResponseDto
|
||||
{
|
||||
Items = items.Select(l => new PushLogItemDto
|
||||
{
|
||||
SendId = l.Id,
|
||||
MessageCode = l.Message?.MessageCode ?? string.Empty,
|
||||
DeviceId = l.DeviceId,
|
||||
Status = l.Status.ToString().ToLowerInvariant(),
|
||||
FailReason = l.FailReason,
|
||||
SentAt = l.SentAt
|
||||
}).ToList(),
|
||||
Pagination = new PaginationDto
|
||||
{
|
||||
Page = request.Page,
|
||||
Size = request.Size,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string ApplyVariables(string template, Dictionary<string, string>? variables)
|
||||
{
|
||||
if (variables == null || variables.Count == 0)
|
||||
|
|
|
|||
13
SPMS.Domain/Interfaces/IPushSendLogRepository.cs
Normal file
13
SPMS.Domain/Interfaces/IPushSendLogRepository.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Enums;
|
||||
|
||||
namespace SPMS.Domain.Interfaces;
|
||||
|
||||
public interface IPushSendLogRepository : IRepository<PushSendLog>
|
||||
{
|
||||
Task<(IReadOnlyList<PushSendLog> Items, int TotalCount)> GetPagedWithMessageAsync(
|
||||
long serviceId, int page, int size,
|
||||
long? messageId = null, long? deviceId = null,
|
||||
PushResult? status = null,
|
||||
DateTime? startDate = null, DateTime? endDate = null);
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ public static class DependencyInjection
|
|||
services.AddScoped<IDeviceRepository, DeviceRepository>();
|
||||
services.AddScoped<IFileRepository, FileRepository>();
|
||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
services.AddScoped<IPushSendLogRepository, PushSendLogRepository>();
|
||||
|
||||
// External Services
|
||||
services.AddScoped<IJwtService, JwtService>();
|
||||
|
|
@ -57,7 +58,8 @@ public static class DependencyInjection
|
|||
// RabbitMQ
|
||||
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
|
||||
services.AddSingleton<RabbitMQConnection>();
|
||||
services.AddHostedService<RabbitMQInitializer>();
|
||||
services.AddSingleton<RabbitMQInitializer>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<RabbitMQInitializer>());
|
||||
services.AddScoped<IPushQueueService, PushQueueService>();
|
||||
|
||||
// Push Senders
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ public class RabbitMQConnection : IAsyncDisposable
|
|||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private IConnection? _connection;
|
||||
|
||||
public bool IsConnected => _connection is { IsOpen: true };
|
||||
|
||||
public RabbitMQConnection(
|
||||
IOptions<RabbitMQSettings> settings,
|
||||
ILogger<RabbitMQConnection> logger)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ using SPMS.Application.Settings;
|
|||
|
||||
namespace SPMS.Infrastructure.Messaging;
|
||||
|
||||
public class RabbitMQInitializer : IHostedService
|
||||
public class RabbitMQInitializer : BackgroundService
|
||||
{
|
||||
private readonly RabbitMQConnection _connection;
|
||||
private readonly RabbitMQSettings _settings;
|
||||
private readonly ILogger<RabbitMQInitializer> _logger;
|
||||
private static readonly TimeSpan RetryInterval = TimeSpan.FromSeconds(30);
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
public RabbitMQInitializer(
|
||||
RabbitMQConnection connection,
|
||||
|
|
@ -22,19 +25,26 @@ public class RabbitMQInitializer : IHostedService
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InitializeAsync(cancellationToken);
|
||||
await InitializeAsync(stoppingToken);
|
||||
IsInitialized = true;
|
||||
_logger.LogInformation("RabbitMQ 초기화 완료");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "RabbitMQ 초기화 실패 — 서버는 계속 실행됩니다. 메시지 발송 시 재연결을 시도합니다.");
|
||||
_logger.LogWarning(ex, "RabbitMQ 초기화 실패 — {RetrySeconds}초 후 재시도합니다.", RetryInterval.TotalSeconds);
|
||||
await Task.Delay(RetryInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
private async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var channel = await _connection.CreateChannelAsync(cancellationToken);
|
||||
|
||||
|
|
@ -87,6 +97,4 @@ public class RabbitMQInitializer : IHostedService
|
|||
_logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: schedule)",
|
||||
_settings.ScheduleQueue, _settings.Exchange);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Enums;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogRepository
|
||||
{
|
||||
public PushSendLogRepository(AppDbContext context) : base(context) { }
|
||||
|
||||
public async Task<(IReadOnlyList<PushSendLog> Items, int TotalCount)> GetPagedWithMessageAsync(
|
||||
long serviceId, int page, int size,
|
||||
long? messageId = null, long? deviceId = null,
|
||||
PushResult? status = null,
|
||||
DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var query = _dbSet
|
||||
.Include(l => l.Message)
|
||||
.Where(l => l.ServiceId == serviceId);
|
||||
|
||||
if (messageId.HasValue)
|
||||
query = query.Where(l => l.MessageId == messageId.Value);
|
||||
|
||||
if (deviceId.HasValue)
|
||||
query = query.Where(l => l.DeviceId == deviceId.Value);
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(l => l.Status == status.Value);
|
||||
|
||||
if (startDate.HasValue)
|
||||
query = query.Where(l => l.SentAt >= startDate.Value);
|
||||
|
||||
if (endDate.HasValue)
|
||||
query = query.Where(l => l.SentAt < endDate.Value.AddDays(1));
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(l => l.SentAt)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user