361 lines
14 KiB
C#
361 lines
14 KiB
C#
using System.Text.Json;
|
|
using SPMS.Application.DTOs.Device;
|
|
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 DeviceService : IDeviceService
|
|
{
|
|
private readonly IDeviceRepository _deviceRepository;
|
|
private readonly ITagRepository _tagRepository;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ITokenCacheService _tokenCache;
|
|
|
|
public DeviceService(
|
|
IDeviceRepository deviceRepository,
|
|
ITagRepository tagRepository,
|
|
IUnitOfWork unitOfWork,
|
|
ITokenCacheService tokenCache)
|
|
{
|
|
_deviceRepository = deviceRepository;
|
|
_tagRepository = tagRepository;
|
|
_unitOfWork = unitOfWork;
|
|
_tokenCache = tokenCache;
|
|
}
|
|
|
|
public async Task<DeviceRegisterResponseDto> RegisterAsync(long serviceId, DeviceRegisterRequestDto request)
|
|
{
|
|
var platform = ParsePlatform(request.Platform);
|
|
var existing = await _deviceRepository.GetByServiceAndTokenAsync(serviceId, request.DeviceToken);
|
|
|
|
if (existing != null)
|
|
{
|
|
existing.ExternalDeviceId = request.DeviceId;
|
|
existing.Platform = platform;
|
|
existing.OsVersion = request.OsVersion;
|
|
existing.AppVersion = request.AppVersion;
|
|
existing.DeviceModel = request.Model;
|
|
existing.IsActive = true;
|
|
existing.UpdatedAt = DateTime.UtcNow;
|
|
_deviceRepository.Update(existing);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
await _tokenCache.InvalidateAsync(serviceId, existing.ExternalDeviceId);
|
|
|
|
return new DeviceRegisterResponseDto { DeviceId = existing.ExternalDeviceId, IsNew = false };
|
|
}
|
|
|
|
var device = new Device
|
|
{
|
|
ServiceId = serviceId,
|
|
ExternalDeviceId = request.DeviceId,
|
|
DeviceToken = request.DeviceToken,
|
|
Platform = platform,
|
|
OsVersion = request.OsVersion,
|
|
AppVersion = request.AppVersion,
|
|
DeviceModel = request.Model,
|
|
PushAgreed = true,
|
|
MarketingAgreed = false,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
await _deviceRepository.AddAsync(device);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
return new DeviceRegisterResponseDto { DeviceId = device.ExternalDeviceId, IsNew = true };
|
|
}
|
|
|
|
public async Task<DeviceInfoResponseDto> GetInfoAsync(long serviceId, DeviceInfoRequestDto request)
|
|
{
|
|
var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId);
|
|
if (device == null)
|
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
|
|
|
return new DeviceInfoResponseDto
|
|
{
|
|
DeviceId = device.ExternalDeviceId,
|
|
DeviceToken = device.DeviceToken,
|
|
Platform = device.Platform.ToString().ToLowerInvariant(),
|
|
OsVersion = device.OsVersion,
|
|
AppVersion = device.AppVersion,
|
|
Model = device.DeviceModel,
|
|
PushAgreed = device.PushAgreed,
|
|
MarketingAgreed = device.MarketingAgreed,
|
|
Tags = await ConvertTagIdsToCodesAsync(device.Tags, serviceId),
|
|
IsActive = device.IsActive,
|
|
LastActiveAt = device.UpdatedAt,
|
|
CreatedAt = device.CreatedAt
|
|
};
|
|
}
|
|
|
|
public async Task UpdateAsync(long serviceId, DeviceUpdateRequestDto request)
|
|
{
|
|
var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId);
|
|
if (device == null)
|
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
|
|
|
if (request.DeviceToken != null)
|
|
device.DeviceToken = request.DeviceToken;
|
|
if (request.OsVersion != null)
|
|
device.OsVersion = request.OsVersion;
|
|
if (request.AppVersion != null)
|
|
device.AppVersion = request.AppVersion;
|
|
|
|
device.UpdatedAt = DateTime.UtcNow;
|
|
_deviceRepository.Update(device);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
await _tokenCache.InvalidateAsync(serviceId, device.ExternalDeviceId);
|
|
}
|
|
|
|
public async Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request)
|
|
{
|
|
var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId);
|
|
if (device == null)
|
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
|
|
|
device.IsActive = false;
|
|
device.UpdatedAt = DateTime.UtcNow;
|
|
_deviceRepository.Update(device);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
await _tokenCache.InvalidateAsync(serviceId, device.ExternalDeviceId);
|
|
}
|
|
|
|
public async Task AdminDeleteAsync(string externalDeviceId)
|
|
{
|
|
var results = await _deviceRepository.FindAsync(d => d.ExternalDeviceId == externalDeviceId);
|
|
var device = results.FirstOrDefault();
|
|
if (device == null)
|
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
|
|
|
device.IsActive = false;
|
|
device.UpdatedAt = DateTime.UtcNow;
|
|
_deviceRepository.Update(device);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
await _tokenCache.InvalidateAsync(device.ServiceId, device.ExternalDeviceId);
|
|
}
|
|
|
|
public async Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request)
|
|
{
|
|
Platform? platform = null;
|
|
if (!string.IsNullOrWhiteSpace(request.Platform))
|
|
platform = ParsePlatform(request.Platform);
|
|
|
|
// tag_code → tag_id 변환
|
|
List<long>? tagIds = null;
|
|
if (request.Tags != null && request.Tags.Count > 0 && serviceId.HasValue)
|
|
{
|
|
var tags = await _tagRepository.GetByTagCodesAndServiceAsync(request.Tags, serviceId.Value);
|
|
tagIds = tags.Select(t => t.Id).ToList();
|
|
}
|
|
|
|
var (items, totalCount) = await _deviceRepository.GetPagedAsync(
|
|
serviceId, request.Page, request.Size,
|
|
platform, request.PushAgreed, request.IsActive, tagIds,
|
|
request.Keyword, request.MarketingAgreed);
|
|
|
|
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
|
|
|
// 응답의 tag_id → tag_code 일괄 변환을 위한 맵 구축
|
|
var allTagIds = new HashSet<long>();
|
|
foreach (var d in items)
|
|
{
|
|
var ids = ParseTagIds(d.Tags);
|
|
if (ids != null) allTagIds.UnionWith(ids);
|
|
}
|
|
var tagIdToCode = await BuildTagIdToCodeMapAsync(allTagIds, serviceId);
|
|
|
|
return new DeviceListResponseDto
|
|
{
|
|
Items = items.Select(d => new DeviceSummaryDto
|
|
{
|
|
DeviceId = d.ExternalDeviceId,
|
|
Platform = d.Platform.ToString().ToLowerInvariant(),
|
|
Model = d.DeviceModel,
|
|
PushAgreed = d.PushAgreed,
|
|
Tags = ConvertTagIdsToCodesSync(d.Tags, tagIdToCode),
|
|
LastActiveAt = d.UpdatedAt,
|
|
DeviceToken = d.DeviceToken,
|
|
ServiceName = d.Service?.ServiceName ?? string.Empty,
|
|
ServiceCode = d.Service?.ServiceCode ?? string.Empty,
|
|
OsVersion = d.OsVersion,
|
|
AppVersion = d.AppVersion,
|
|
MarketingAgreed = d.MarketingAgreed,
|
|
IsActive = d.IsActive,
|
|
CreatedAt = d.CreatedAt
|
|
}).ToList(),
|
|
Pagination = new DTOs.Notice.PaginationDto
|
|
{
|
|
Page = request.Page,
|
|
Size = request.Size,
|
|
TotalCount = totalCount,
|
|
TotalPages = totalPages
|
|
}
|
|
};
|
|
}
|
|
|
|
public async Task<byte[]> ExportAsync(long? serviceId, DeviceExportRequestDto request)
|
|
{
|
|
Platform? platform = null;
|
|
if (!string.IsNullOrWhiteSpace(request.Platform))
|
|
platform = ParsePlatform(request.Platform);
|
|
|
|
// tag_code → tag_id 변환
|
|
List<long>? tagIds = null;
|
|
if (request.Tags != null && request.Tags.Count > 0 && serviceId.HasValue)
|
|
{
|
|
var tags = await _tagRepository.GetByTagCodesAndServiceAsync(request.Tags, serviceId.Value);
|
|
tagIds = tags.Select(t => t.Id).ToList();
|
|
}
|
|
|
|
var items = await _deviceRepository.GetAllFilteredAsync(
|
|
serviceId, platform, request.PushAgreed, request.IsActive,
|
|
tagIds, request.Keyword, request.MarketingAgreed);
|
|
|
|
using var workbook = new ClosedXML.Excel.XLWorkbook();
|
|
var ws = workbook.Worksheets.Add("기기 목록");
|
|
|
|
ws.Cell(1, 1).Value = "Device ID";
|
|
ws.Cell(1, 2).Value = "Device Token";
|
|
ws.Cell(1, 3).Value = "서비스명";
|
|
ws.Cell(1, 4).Value = "서비스코드";
|
|
ws.Cell(1, 5).Value = "플랫폼";
|
|
ws.Cell(1, 6).Value = "모델";
|
|
ws.Cell(1, 7).Value = "OS 버전";
|
|
ws.Cell(1, 8).Value = "앱 버전";
|
|
ws.Cell(1, 9).Value = "푸시 동의";
|
|
ws.Cell(1, 10).Value = "광고 동의";
|
|
ws.Cell(1, 11).Value = "활성 상태";
|
|
ws.Cell(1, 12).Value = "태그";
|
|
ws.Cell(1, 13).Value = "등록일";
|
|
ws.Cell(1, 14).Value = "마지막 활동";
|
|
ws.Row(1).Style.Font.Bold = true;
|
|
|
|
var row = 2;
|
|
foreach (var d in items)
|
|
{
|
|
ws.Cell(row, 1).Value = d.Id;
|
|
ws.Cell(row, 2).Value = d.DeviceToken;
|
|
ws.Cell(row, 3).Value = d.Service?.ServiceName ?? string.Empty;
|
|
ws.Cell(row, 4).Value = d.Service?.ServiceCode ?? string.Empty;
|
|
ws.Cell(row, 5).Value = d.Platform.ToString().ToLowerInvariant();
|
|
ws.Cell(row, 6).Value = d.DeviceModel ?? string.Empty;
|
|
ws.Cell(row, 7).Value = d.OsVersion ?? string.Empty;
|
|
ws.Cell(row, 8).Value = d.AppVersion ?? string.Empty;
|
|
ws.Cell(row, 9).Value = d.PushAgreed ? "Y" : "N";
|
|
ws.Cell(row, 10).Value = d.MarketingAgreed ? "Y" : "N";
|
|
ws.Cell(row, 11).Value = d.IsActive ? "활성" : "비활성";
|
|
ws.Cell(row, 12).Value = d.Tags ?? string.Empty;
|
|
ws.Cell(row, 13).Value = d.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
|
ws.Cell(row, 14).Value = d.UpdatedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? string.Empty;
|
|
row++;
|
|
}
|
|
ws.Columns().AdjustToContents();
|
|
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
public async Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request)
|
|
{
|
|
var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId);
|
|
if (device == null)
|
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
|
|
|
// tag_code → tag_id 변환
|
|
var tags = await _tagRepository.GetByTagCodesAndServiceAsync(request.Tags, serviceId);
|
|
var tagIds = tags.Select(t => t.Id).ToList();
|
|
|
|
device.Tags = tagIds.Count > 0 ? JsonSerializer.Serialize(tagIds) : null;
|
|
device.UpdatedAt = DateTime.UtcNow;
|
|
_deviceRepository.Update(device);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request)
|
|
{
|
|
var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId);
|
|
if (device == null)
|
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
|
|
|
device.PushAgreed = request.PushAgreed;
|
|
device.MarketingAgreed = request.MarketingAgreed;
|
|
device.AgreeUpdatedAt = DateTime.UtcNow;
|
|
if (request.MarketingAgreed != device.MarketingAgreed)
|
|
device.MktAgreeUpdatedAt = DateTime.UtcNow;
|
|
_deviceRepository.Update(device);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
await _tokenCache.InvalidateAsync(serviceId, device.ExternalDeviceId);
|
|
}
|
|
|
|
private static Platform ParsePlatform(string platform)
|
|
{
|
|
return platform.ToLowerInvariant() switch
|
|
{
|
|
"ios" => Platform.iOS,
|
|
"android" => Platform.Android,
|
|
"web" => Platform.Web,
|
|
_ => throw new SpmsException(ErrorCodes.BadRequest, "유효하지 않은 플랫폼입니다.", 400)
|
|
};
|
|
}
|
|
|
|
private static List<long>? ParseTagIds(string? tagsJson)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(tagsJson))
|
|
return null;
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<List<long>>(tagsJson);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DB에 저장된 tag_id JSON을 tag_code 목록으로 변환 (단일 디바이스용)
|
|
/// </summary>
|
|
private async Task<List<string>?> ConvertTagIdsToCodesAsync(string? tagsJson, long serviceId)
|
|
{
|
|
var tagIds = ParseTagIds(tagsJson);
|
|
if (tagIds == null || tagIds.Count == 0)
|
|
return null;
|
|
|
|
var tags = await _tagRepository.FindAsync(t => t.ServiceId == serviceId && tagIds.Contains(t.Id));
|
|
return tags.Select(t => t.TagCode).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 미리 구축한 tagId→tagCode 맵을 사용하여 동기 변환 (목록 조회용)
|
|
/// </summary>
|
|
private static List<string>? ConvertTagIdsToCodesSync(string? tagsJson, Dictionary<long, string> tagIdToCode)
|
|
{
|
|
var tagIds = ParseTagIds(tagsJson);
|
|
if (tagIds == null || tagIds.Count == 0)
|
|
return null;
|
|
|
|
return tagIds
|
|
.Where(id => tagIdToCode.ContainsKey(id))
|
|
.Select(id => tagIdToCode[id])
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// tag_id 집합으로부터 tag_id → tag_code 매핑 딕셔너리 구축
|
|
/// </summary>
|
|
private async Task<Dictionary<long, string>> BuildTagIdToCodeMapAsync(HashSet<long> tagIds, long? serviceId)
|
|
{
|
|
if (tagIds.Count == 0)
|
|
return new Dictionary<long, string>();
|
|
|
|
var tagIdList = tagIds.ToList();
|
|
var tags = await _tagRepository.FindAsync(t => tagIdList.Contains(t.Id));
|
|
return tags.ToDictionary(t => t.Id, t => t.TagCode);
|
|
}
|
|
}
|