- POST /v1/in/push/log 엔드포인트 추가 - PushSendLogRepository (페이징, 필터링: message_code, device_id, status, 날짜범위) - PushService.GetLogAsync 구현 - 누락된 Push DTO 파일 포함 (PushSendRequestDto, PushSendResponseDto, PushSendTagRequestDto)
287 lines
10 KiB
C#
287 lines
10 KiB
C#
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;
|
|
|
|
namespace SPMS.Application.Services;
|
|
|
|
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,
|
|
IPushSendLogRepository pushSendLogRepository)
|
|
{
|
|
_messageRepository = messageRepository;
|
|
_pushQueueService = pushQueueService;
|
|
_scheduleCancelStore = scheduleCancelStore;
|
|
_pushSendLogRepository = pushSendLogRepository;
|
|
}
|
|
|
|
public async Task<PushSendResponseDto> SendAsync(long serviceId, PushSendRequestDto request)
|
|
{
|
|
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
|
if (message == null)
|
|
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
|
|
|
var title = ApplyVariables(message.Title, request.Variables);
|
|
var body = ApplyVariables(message.Body, request.Variables);
|
|
|
|
var requestId = Guid.NewGuid().ToString("N");
|
|
|
|
var pushMessage = new PushMessageDto
|
|
{
|
|
MessageId = message.Id.ToString(),
|
|
RequestId = requestId,
|
|
ServiceId = serviceId,
|
|
SendType = "single",
|
|
Title = title,
|
|
Body = body,
|
|
ImageUrl = message.ImageUrl,
|
|
LinkUrl = message.LinkUrl,
|
|
CustomData = ParseCustomData(message.CustomData),
|
|
Target = new PushTargetDto
|
|
{
|
|
Type = "device_ids",
|
|
Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId })
|
|
},
|
|
CreatedBy = message.CreatedBy,
|
|
CreatedAt = DateTime.UtcNow.ToString("o")
|
|
};
|
|
|
|
await _pushQueueService.PublishPushMessageAsync(pushMessage);
|
|
|
|
return new PushSendResponseDto
|
|
{
|
|
RequestId = requestId,
|
|
SendType = "single",
|
|
Status = "queued"
|
|
};
|
|
}
|
|
|
|
public async Task<PushSendResponseDto> SendByTagAsync(long serviceId, PushSendTagRequestDto request)
|
|
{
|
|
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
|
if (message == null)
|
|
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
|
|
|
var requestId = Guid.NewGuid().ToString("N");
|
|
|
|
var pushMessage = new PushMessageDto
|
|
{
|
|
MessageId = message.Id.ToString(),
|
|
RequestId = requestId,
|
|
ServiceId = serviceId,
|
|
SendType = "group",
|
|
Title = message.Title,
|
|
Body = message.Body,
|
|
ImageUrl = message.ImageUrl,
|
|
LinkUrl = message.LinkUrl,
|
|
CustomData = ParseCustomData(message.CustomData),
|
|
Target = new PushTargetDto
|
|
{
|
|
Type = "tags",
|
|
Value = JsonSerializer.SerializeToElement(new
|
|
{
|
|
tags = request.Tags,
|
|
match = request.TagMatch
|
|
})
|
|
},
|
|
CreatedBy = message.CreatedBy,
|
|
CreatedAt = DateTime.UtcNow.ToString("o")
|
|
};
|
|
|
|
await _pushQueueService.PublishPushMessageAsync(pushMessage);
|
|
|
|
return new PushSendResponseDto
|
|
{
|
|
RequestId = requestId,
|
|
SendType = "group",
|
|
Status = "queued"
|
|
};
|
|
}
|
|
|
|
public async Task<PushScheduleResponseDto> ScheduleAsync(long serviceId, PushScheduleRequestDto request)
|
|
{
|
|
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
|
if (message == null)
|
|
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
|
|
|
var sendType = request.SendType.ToLowerInvariant();
|
|
if (sendType != "single" && sendType != "tag")
|
|
throw new SpmsException(ErrorCodes.BadRequest, "send_type은 single 또는 tag만 허용됩니다.", 400);
|
|
|
|
if (sendType == "single" && request.DeviceId == null)
|
|
throw new SpmsException(ErrorCodes.BadRequest, "send_type=single 시 device_id는 필수입니다.", 400);
|
|
|
|
if (sendType == "tag" && (request.Tags == null || request.Tags.Count == 0))
|
|
throw new SpmsException(ErrorCodes.BadRequest, "send_type=tag 시 tags는 필수입니다.", 400);
|
|
|
|
var requestId = Guid.NewGuid().ToString("N");
|
|
var scheduleId = $"sch_{DateTime.UtcNow:yyyyMMdd}_{requestId[..8]}";
|
|
|
|
var title = ApplyVariables(message.Title, request.Variables);
|
|
var body = ApplyVariables(message.Body, request.Variables);
|
|
|
|
PushTargetDto target;
|
|
if (sendType == "single")
|
|
{
|
|
target = new PushTargetDto
|
|
{
|
|
Type = "device_ids",
|
|
Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId!.Value })
|
|
};
|
|
}
|
|
else
|
|
{
|
|
target = new PushTargetDto
|
|
{
|
|
Type = "tags",
|
|
Value = JsonSerializer.SerializeToElement(new { tags = request.Tags, match = "or" })
|
|
};
|
|
}
|
|
|
|
var pushMessage = new PushMessageDto
|
|
{
|
|
MessageId = message.Id.ToString(),
|
|
RequestId = requestId,
|
|
ServiceId = serviceId,
|
|
SendType = sendType,
|
|
Title = title,
|
|
Body = body,
|
|
ImageUrl = message.ImageUrl,
|
|
LinkUrl = message.LinkUrl,
|
|
CustomData = ParseCustomData(message.CustomData),
|
|
Target = target,
|
|
CreatedBy = message.CreatedBy,
|
|
CreatedAt = DateTime.UtcNow.ToString("o")
|
|
};
|
|
|
|
var scheduleMessage = new ScheduleMessageDto
|
|
{
|
|
ScheduleId = scheduleId,
|
|
MessageId = message.Id.ToString(),
|
|
ServiceId = serviceId,
|
|
ScheduledAt = request.ScheduledAt,
|
|
PushMessage = pushMessage
|
|
};
|
|
|
|
await _pushQueueService.PublishScheduleMessageAsync(scheduleMessage);
|
|
|
|
return new PushScheduleResponseDto
|
|
{
|
|
ScheduleId = scheduleId,
|
|
ScheduledAt = request.ScheduledAt,
|
|
Status = "scheduled"
|
|
};
|
|
}
|
|
|
|
public async Task CancelScheduleAsync(PushScheduleCancelRequestDto request)
|
|
{
|
|
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)
|
|
return template;
|
|
|
|
var result = template;
|
|
foreach (var (key, value) in variables)
|
|
{
|
|
result = result.Replace($"{{{{{key}}}}}", value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static Dictionary<string, object>? ParseCustomData(string? customData)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(customData))
|
|
return null;
|
|
|
|
return JsonSerializer.Deserialize<Dictionary<string, object>>(customData);
|
|
}
|
|
}
|