SPMS_API/SPMS.Application/Services/DeviceService.cs

358 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.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.Id);
return new DeviceRegisterResponseDto { DeviceId = existing.Id, IsNew = false };
}
var device = new Device
{
ServiceId = serviceId,
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.Id, IsNew = true };
}
public async Task<DeviceInfoResponseDto> GetInfoAsync(long serviceId, DeviceInfoRequestDto request)
{
var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId);
if (device == null)
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
return new DeviceInfoResponseDto
{
DeviceId = device.Id,
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.GetByIdAndServiceAsync(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.Id);
}
public async Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request)
{
var device = await _deviceRepository.GetByIdAndServiceAsync(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.Id);
}
public async Task AdminDeleteAsync(long deviceId)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
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.Id);
}
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.Id,
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.GetByIdAndServiceAsync(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.GetByIdAndServiceAsync(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.Id);
}
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);
}
}