parent
165328b7df
commit
71cd7a5e98
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -62,4 +62,5 @@ Dockerfile
|
||||||
Documents/
|
Documents/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
TASKS.md
|
TASKS.md
|
||||||
|
TODO.md
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
|
@ -43,7 +43,8 @@ public class TagController : ControllerBase
|
||||||
[SwaggerOperation(Summary = "태그 수정", Description = "태그 설명을 수정합니다. 태그명(Name)은 변경 불가.")]
|
[SwaggerOperation(Summary = "태그 수정", Description = "태그 설명을 수정합니다. 태그명(Name)은 변경 불가.")]
|
||||||
public async Task<IActionResult> UpdateAsync([FromBody] UpdateTagRequestDto request)
|
public async Task<IActionResult> UpdateAsync([FromBody] UpdateTagRequestDto request)
|
||||||
{
|
{
|
||||||
var result = await _tagService.UpdateAsync(request);
|
var serviceId = GetServiceId();
|
||||||
|
var result = await _tagService.UpdateAsync(serviceId, request);
|
||||||
return Ok(ApiResponse<TagResponseDto>.Success(result, "태그 수정 성공"));
|
return Ok(ApiResponse<TagResponseDto>.Success(result, "태그 수정 성공"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +52,8 @@ public class TagController : ControllerBase
|
||||||
[SwaggerOperation(Summary = "태그 삭제", Description = "태그를 삭제합니다.")]
|
[SwaggerOperation(Summary = "태그 삭제", Description = "태그를 삭제합니다.")]
|
||||||
public async Task<IActionResult> DeleteAsync([FromBody] DeleteTagRequestDto request)
|
public async Task<IActionResult> DeleteAsync([FromBody] DeleteTagRequestDto request)
|
||||||
{
|
{
|
||||||
await _tagService.DeleteAsync(request);
|
var serviceId = GetServiceId();
|
||||||
|
await _tagService.DeleteAsync(serviceId, request);
|
||||||
return Ok(ApiResponse.Success());
|
return Ok(ApiResponse.Success());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +64,13 @@ public class TagController : ControllerBase
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long GetServiceId()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
|
||||||
|
return serviceId;
|
||||||
|
throw new SpmsException(ErrorCodes.ServiceScopeRequired, "X-Service-Code 헤더가 필요합니다.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
private long GetAdminId()
|
private long GetAdminId()
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public class ServiceCodeMiddleware
|
||||||
if (path.StartsWithSegments("/v1/in/stats") ||
|
if (path.StartsWithSegments("/v1/in/stats") ||
|
||||||
path.StartsWithSegments("/v1/in/device/list") ||
|
path.StartsWithSegments("/v1/in/device/list") ||
|
||||||
path.StartsWithSegments("/v1/in/message/list") ||
|
path.StartsWithSegments("/v1/in/message/list") ||
|
||||||
path.StartsWithSegments("/v1/in/tag"))
|
path == "/v1/in/tag/list")
|
||||||
{
|
{
|
||||||
if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) &&
|
if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) &&
|
||||||
!string.IsNullOrWhiteSpace(optionalCode))
|
!string.IsNullOrWhiteSpace(optionalCode))
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ public class DeviceExportRequestDto
|
||||||
public bool? PushAgreed { get; set; }
|
public bool? PushAgreed { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
public List<int>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("is_active")]
|
[JsonPropertyName("is_active")]
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public class DeviceInfoResponseDto
|
||||||
public bool MarketingAgreed { get; set; }
|
public bool MarketingAgreed { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
public List<int>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("is_active")]
|
[JsonPropertyName("is_active")]
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public class DeviceListRequestDto
|
||||||
public bool? PushAgreed { get; set; }
|
public bool? PushAgreed { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
public List<int>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("is_active")]
|
[JsonPropertyName("is_active")]
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public class DeviceSummaryDto
|
||||||
public bool PushAgreed { get; set; }
|
public bool PushAgreed { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
public List<int>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("last_active_at")]
|
[JsonPropertyName("last_active_at")]
|
||||||
public DateTime? LastActiveAt { get; set; }
|
public DateTime? LastActiveAt { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ public class DeviceTagsRequestDto
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
public List<int> Tags { get; set; } = new();
|
public List<string> Tags { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public class PushScheduleRequestDto
|
||||||
|
|
||||||
public long? DeviceId { get; set; }
|
public long? DeviceId { get; set; }
|
||||||
|
|
||||||
public List<int>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string ScheduledAt { get; set; } = string.Empty;
|
public string ScheduledAt { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ public class PushSendTagRequestDto
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[MinLength(1)]
|
[MinLength(1)]
|
||||||
public List<int> Tags { get; set; } = [];
|
public List<string> Tags { get; set; } = [];
|
||||||
|
|
||||||
public string TagMatch { get; set; } = "or";
|
public string TagMatch { get; set; } = "or";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ namespace SPMS.Application.DTOs.Tag;
|
||||||
public class DeleteTagRequestDto
|
public class DeleteTagRequestDto
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[JsonPropertyName("tag_id")]
|
[JsonPropertyName("tag_code")]
|
||||||
public long TagId { get; set; }
|
public string TagCode { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ public class TagSummaryDto
|
||||||
[JsonPropertyName("tag_index")]
|
[JsonPropertyName("tag_index")]
|
||||||
public int TagIndex { get; set; }
|
public int TagIndex { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tag_id")]
|
[JsonPropertyName("tag_code")]
|
||||||
public long TagId { get; set; }
|
public string TagCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("tag_name")]
|
[JsonPropertyName("tag_name")]
|
||||||
public string TagName { get; set; } = string.Empty;
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ namespace SPMS.Application.DTOs.Tag;
|
||||||
|
|
||||||
public class TagResponseDto
|
public class TagResponseDto
|
||||||
{
|
{
|
||||||
[JsonPropertyName("tag_id")]
|
[JsonPropertyName("tag_code")]
|
||||||
public long TagId { get; set; }
|
public string TagCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("tag_name")]
|
[JsonPropertyName("tag_name")]
|
||||||
public string TagName { get; set; } = string.Empty;
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ namespace SPMS.Application.DTOs.Tag;
|
||||||
public class UpdateTagRequestDto
|
public class UpdateTagRequestDto
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[JsonPropertyName("tag_id")]
|
[JsonPropertyName("tag_code")]
|
||||||
public long TagId { get; set; }
|
public string TagCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
[JsonPropertyName("description")]
|
[JsonPropertyName("description")]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ public interface ITagService
|
||||||
{
|
{
|
||||||
Task<TagListResponseDto> GetListAsync(long? scopeServiceId, TagListRequestDto request);
|
Task<TagListResponseDto> GetListAsync(long? scopeServiceId, TagListRequestDto request);
|
||||||
Task<TagResponseDto> CreateAsync(long adminId, CreateTagRequestDto request);
|
Task<TagResponseDto> CreateAsync(long adminId, CreateTagRequestDto request);
|
||||||
Task<TagResponseDto> UpdateAsync(UpdateTagRequestDto request);
|
Task<TagResponseDto> UpdateAsync(long serviceId, UpdateTagRequestDto request);
|
||||||
Task DeleteAsync(DeleteTagRequestDto request);
|
Task DeleteAsync(long serviceId, DeleteTagRequestDto request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,18 @@ namespace SPMS.Application.Services;
|
||||||
public class DeviceService : IDeviceService
|
public class DeviceService : IDeviceService
|
||||||
{
|
{
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
|
private readonly ITagRepository _tagRepository;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITokenCacheService _tokenCache;
|
private readonly ITokenCacheService _tokenCache;
|
||||||
|
|
||||||
public DeviceService(
|
public DeviceService(
|
||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
|
ITagRepository tagRepository,
|
||||||
IUnitOfWork unitOfWork,
|
IUnitOfWork unitOfWork,
|
||||||
ITokenCacheService tokenCache)
|
ITokenCacheService tokenCache)
|
||||||
{
|
{
|
||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
|
_tagRepository = tagRepository;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_tokenCache = tokenCache;
|
_tokenCache = tokenCache;
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +83,7 @@ public class DeviceService : IDeviceService
|
||||||
Model = device.DeviceModel,
|
Model = device.DeviceModel,
|
||||||
PushAgreed = device.PushAgreed,
|
PushAgreed = device.PushAgreed,
|
||||||
MarketingAgreed = device.MarketingAgreed,
|
MarketingAgreed = device.MarketingAgreed,
|
||||||
Tags = ParseTags(device.Tags),
|
Tags = await ConvertTagIdsToCodesAsync(device.Tags, serviceId),
|
||||||
IsActive = device.IsActive,
|
IsActive = device.IsActive,
|
||||||
LastActiveAt = device.UpdatedAt,
|
LastActiveAt = device.UpdatedAt,
|
||||||
CreatedAt = device.CreatedAt
|
CreatedAt = device.CreatedAt
|
||||||
|
|
@ -138,13 +141,30 @@ public class DeviceService : IDeviceService
|
||||||
if (!string.IsNullOrWhiteSpace(request.Platform))
|
if (!string.IsNullOrWhiteSpace(request.Platform))
|
||||||
platform = ParsePlatform(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(
|
var (items, totalCount) = await _deviceRepository.GetPagedAsync(
|
||||||
serviceId, request.Page, request.Size,
|
serviceId, request.Page, request.Size,
|
||||||
platform, request.PushAgreed, request.IsActive, request.Tags,
|
platform, request.PushAgreed, request.IsActive, tagIds,
|
||||||
request.Keyword, request.MarketingAgreed);
|
request.Keyword, request.MarketingAgreed);
|
||||||
|
|
||||||
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
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
|
return new DeviceListResponseDto
|
||||||
{
|
{
|
||||||
Items = items.Select(d => new DeviceSummaryDto
|
Items = items.Select(d => new DeviceSummaryDto
|
||||||
|
|
@ -153,7 +173,7 @@ public class DeviceService : IDeviceService
|
||||||
Platform = d.Platform.ToString().ToLowerInvariant(),
|
Platform = d.Platform.ToString().ToLowerInvariant(),
|
||||||
Model = d.DeviceModel,
|
Model = d.DeviceModel,
|
||||||
PushAgreed = d.PushAgreed,
|
PushAgreed = d.PushAgreed,
|
||||||
Tags = ParseTags(d.Tags),
|
Tags = ConvertTagIdsToCodesSync(d.Tags, tagIdToCode),
|
||||||
LastActiveAt = d.UpdatedAt,
|
LastActiveAt = d.UpdatedAt,
|
||||||
DeviceToken = d.DeviceToken,
|
DeviceToken = d.DeviceToken,
|
||||||
ServiceName = d.Service?.ServiceName ?? string.Empty,
|
ServiceName = d.Service?.ServiceName ?? string.Empty,
|
||||||
|
|
@ -180,9 +200,17 @@ public class DeviceService : IDeviceService
|
||||||
if (!string.IsNullOrWhiteSpace(request.Platform))
|
if (!string.IsNullOrWhiteSpace(request.Platform))
|
||||||
platform = ParsePlatform(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(
|
var items = await _deviceRepository.GetAllFilteredAsync(
|
||||||
serviceId, platform, request.PushAgreed, request.IsActive,
|
serviceId, platform, request.PushAgreed, request.IsActive,
|
||||||
request.Tags, request.Keyword, request.MarketingAgreed);
|
tagIds, request.Keyword, request.MarketingAgreed);
|
||||||
|
|
||||||
using var workbook = new ClosedXML.Excel.XLWorkbook();
|
using var workbook = new ClosedXML.Excel.XLWorkbook();
|
||||||
var ws = workbook.Worksheets.Add("기기 목록");
|
var ws = workbook.Worksheets.Add("기기 목록");
|
||||||
|
|
@ -235,7 +263,11 @@ public class DeviceService : IDeviceService
|
||||||
if (device == null)
|
if (device == null)
|
||||||
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404);
|
||||||
|
|
||||||
device.Tags = JsonSerializer.Serialize(request.Tags);
|
// 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;
|
device.UpdatedAt = DateTime.UtcNow;
|
||||||
_deviceRepository.Update(device);
|
_deviceRepository.Update(device);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
@ -268,17 +300,58 @@ public class DeviceService : IDeviceService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<int>? ParseTags(string? tagsJson)
|
private static List<long>? ParseTagIds(string? tagsJson)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(tagsJson))
|
if (string.IsNullOrWhiteSpace(tagsJson))
|
||||||
return null;
|
return null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<List<int>>(tagsJson);
|
return JsonSerializer.Deserialize<List<long>>(tagsJson);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return null;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using SPMS.Application.DTOs.Notice;
|
using SPMS.Application.DTOs.Notice;
|
||||||
using SPMS.Application.DTOs.Tag;
|
using SPMS.Application.DTOs.Tag;
|
||||||
|
|
@ -17,6 +18,8 @@ public class TagService : ITagService
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
private const int MaxTagsPerService = 10;
|
private const int MaxTagsPerService = 10;
|
||||||
|
private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
private const int TagCodeLength = 4;
|
||||||
|
|
||||||
public TagService(
|
public TagService(
|
||||||
ITagRepository tagRepository,
|
ITagRepository tagRepository,
|
||||||
|
|
@ -32,7 +35,6 @@ public class TagService : ITagService
|
||||||
|
|
||||||
public async Task<TagListResponseDto> GetListAsync(long? scopeServiceId, TagListRequestDto request)
|
public async Task<TagListResponseDto> GetListAsync(long? scopeServiceId, TagListRequestDto request)
|
||||||
{
|
{
|
||||||
// scopeServiceId가 있으면 해당 서비스만, request.ServiceId도 추가 필터
|
|
||||||
var effectiveServiceId = scopeServiceId ?? request.ServiceId;
|
var effectiveServiceId = scopeServiceId ?? request.ServiceId;
|
||||||
|
|
||||||
var (items, totalCount) = await _tagRepository.GetTagListAsync(
|
var (items, totalCount) = await _tagRepository.GetTagListAsync(
|
||||||
|
|
@ -58,7 +60,7 @@ public class TagService : ITagService
|
||||||
Items = items.Select(t => new TagSummaryDto
|
Items = items.Select(t => new TagSummaryDto
|
||||||
{
|
{
|
||||||
TagIndex = serviceTagOrders[t.ServiceId].IndexOf(t.Id) + 1,
|
TagIndex = serviceTagOrders[t.ServiceId].IndexOf(t.Id) + 1,
|
||||||
TagId = t.Id,
|
TagCode = t.TagCode,
|
||||||
TagName = t.Name,
|
TagName = t.Name,
|
||||||
Description = t.Description,
|
Description = t.Description,
|
||||||
ServiceId = t.ServiceId,
|
ServiceId = t.ServiceId,
|
||||||
|
|
@ -93,9 +95,13 @@ public class TagService : ITagService
|
||||||
if (currentCount >= MaxTagsPerService)
|
if (currentCount >= MaxTagsPerService)
|
||||||
throw new SpmsException(ErrorCodes.TagLimitExceeded, $"서비스당 태그는 최대 {MaxTagsPerService}개까지 생성할 수 있습니다.");
|
throw new SpmsException(ErrorCodes.TagLimitExceeded, $"서비스당 태그는 최대 {MaxTagsPerService}개까지 생성할 수 있습니다.");
|
||||||
|
|
||||||
|
// TagCode 생성
|
||||||
|
var tagCode = await GenerateUniqueTagCodeAsync(request.ServiceId);
|
||||||
|
|
||||||
var tag = new Tag
|
var tag = new Tag
|
||||||
{
|
{
|
||||||
ServiceId = request.ServiceId,
|
ServiceId = request.ServiceId,
|
||||||
|
TagCode = tagCode,
|
||||||
Name = request.TagName,
|
Name = request.TagName,
|
||||||
Description = request.Description,
|
Description = request.Description,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
|
@ -107,7 +113,7 @@ public class TagService : ITagService
|
||||||
|
|
||||||
return new TagResponseDto
|
return new TagResponseDto
|
||||||
{
|
{
|
||||||
TagId = tag.Id,
|
TagCode = tag.TagCode,
|
||||||
TagName = tag.Name,
|
TagName = tag.Name,
|
||||||
Description = tag.Description,
|
Description = tag.Description,
|
||||||
ServiceId = tag.ServiceId,
|
ServiceId = tag.ServiceId,
|
||||||
|
|
@ -117,9 +123,9 @@ public class TagService : ITagService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TagResponseDto> UpdateAsync(UpdateTagRequestDto request)
|
public async Task<TagResponseDto> UpdateAsync(long serviceId, UpdateTagRequestDto request)
|
||||||
{
|
{
|
||||||
var tag = await _tagRepository.GetByIdWithServiceAsync(request.TagId);
|
var tag = await _tagRepository.GetByTagCodeAndServiceAsync(request.TagCode, serviceId);
|
||||||
if (tag == null)
|
if (tag == null)
|
||||||
throw new SpmsException(ErrorCodes.TagNotFound, "태그를 찾을 수 없습니다.", 404);
|
throw new SpmsException(ErrorCodes.TagNotFound, "태그를 찾을 수 없습니다.", 404);
|
||||||
|
|
||||||
|
|
@ -131,7 +137,7 @@ public class TagService : ITagService
|
||||||
|
|
||||||
return new TagResponseDto
|
return new TagResponseDto
|
||||||
{
|
{
|
||||||
TagId = tag.Id,
|
TagCode = tag.TagCode,
|
||||||
TagName = tag.Name,
|
TagName = tag.Name,
|
||||||
Description = tag.Description,
|
Description = tag.Description,
|
||||||
ServiceId = tag.ServiceId,
|
ServiceId = tag.ServiceId,
|
||||||
|
|
@ -141,9 +147,9 @@ public class TagService : ITagService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(DeleteTagRequestDto request)
|
public async Task DeleteAsync(long serviceId, DeleteTagRequestDto request)
|
||||||
{
|
{
|
||||||
var tag = await _tagRepository.GetByIdWithServiceAsync(request.TagId);
|
var tag = await _tagRepository.GetByTagCodeAndServiceAsync(request.TagCode, serviceId);
|
||||||
if (tag == null)
|
if (tag == null)
|
||||||
throw new SpmsException(ErrorCodes.TagNotFound, "태그를 찾을 수 없습니다.", 404);
|
throw new SpmsException(ErrorCodes.TagNotFound, "태그를 찾을 수 없습니다.", 404);
|
||||||
|
|
||||||
|
|
@ -152,11 +158,11 @@ public class TagService : ITagService
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 해당 태그를 참조하는 디바이스의 Tags에서 tagId 제거
|
// 해당 태그를 참조하는 디바이스의 Tags에서 tagId 제거
|
||||||
var devices = await _deviceRepository.GetDevicesByTagIdAsync(request.TagId);
|
var devices = await _deviceRepository.GetDevicesByTagIdAsync(tag.Id);
|
||||||
foreach (var device in devices)
|
foreach (var device in devices)
|
||||||
{
|
{
|
||||||
var tagList = JsonSerializer.Deserialize<List<int>>(device.Tags!) ?? new();
|
var tagList = JsonSerializer.Deserialize<List<long>>(device.Tags!) ?? new();
|
||||||
tagList.Remove((int)request.TagId);
|
tagList.Remove(tag.Id);
|
||||||
device.Tags = tagList.Count > 0 ? JsonSerializer.Serialize(tagList) : null;
|
device.Tags = tagList.Count > 0 ? JsonSerializer.Serialize(tagList) : null;
|
||||||
device.UpdatedAt = DateTime.UtcNow;
|
device.UpdatedAt = DateTime.UtcNow;
|
||||||
_deviceRepository.Update(device);
|
_deviceRepository.Update(device);
|
||||||
|
|
@ -173,4 +179,29 @@ public class TagService : ITagService
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateUniqueTagCodeAsync(long serviceId)
|
||||||
|
{
|
||||||
|
var buffer = new byte[TagCodeLength];
|
||||||
|
|
||||||
|
for (var attempt = 0; attempt < 10; attempt++)
|
||||||
|
{
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = new char[TagCodeLength];
|
||||||
|
for (var i = 0; i < TagCodeLength; i++)
|
||||||
|
{
|
||||||
|
code[i] = Alphabet[buffer[i] % Alphabet.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagCode = new string(code);
|
||||||
|
if (!await _tagRepository.TagCodeExistsInServiceAsync(serviceId, tagCode))
|
||||||
|
return tagCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SpmsException(ErrorCodes.InternalError, "태그 코드 생성에 실패했습니다. 잠시 후 다시 시도해주세요.", 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ namespace SPMS.Domain.Entities;
|
||||||
public class Tag : BaseEntity
|
public class Tag : BaseEntity
|
||||||
{
|
{
|
||||||
public long ServiceId { get; set; }
|
public long ServiceId { get; set; }
|
||||||
|
public string TagCode { get; set; } = string.Empty;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ public interface IDeviceRepository : IRepository<Device>
|
||||||
Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
|
Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
|
||||||
long? serviceId, int page, int size,
|
long? serviceId, int page, int size,
|
||||||
Platform? platform = null, bool? pushAgreed = null,
|
Platform? platform = null, bool? pushAgreed = null,
|
||||||
bool? isActive = null, List<int>? tags = null,
|
bool? isActive = null, List<long>? tagIds = null,
|
||||||
string? keyword = null, bool? marketingAgreed = null);
|
string? keyword = null, bool? marketingAgreed = null);
|
||||||
Task<IReadOnlyList<Device>> GetAllFilteredAsync(
|
Task<IReadOnlyList<Device>> GetAllFilteredAsync(
|
||||||
long? serviceId,
|
long? serviceId,
|
||||||
Platform? platform = null, bool? pushAgreed = null,
|
Platform? platform = null, bool? pushAgreed = null,
|
||||||
bool? isActive = null, List<int>? tags = null,
|
bool? isActive = null, List<long>? tagIds = null,
|
||||||
string? keyword = null, bool? marketingAgreed = null);
|
string? keyword = null, bool? marketingAgreed = null);
|
||||||
Task<Dictionary<long, int>> GetDeviceCountsByTagIdsAsync(IEnumerable<long> tagIds);
|
Task<Dictionary<long, int>> GetDeviceCountsByTagIdsAsync(IEnumerable<long> tagIds);
|
||||||
Task<IReadOnlyList<Device>> GetDevicesByTagIdAsync(long tagId);
|
Task<IReadOnlyList<Device>> GetDevicesByTagIdAsync(long tagId);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ namespace SPMS.Domain.Interfaces;
|
||||||
public interface ITagRepository : IRepository<Tag>
|
public interface ITagRepository : IRepository<Tag>
|
||||||
{
|
{
|
||||||
Task<Tag?> GetByIdWithServiceAsync(long id);
|
Task<Tag?> GetByIdWithServiceAsync(long id);
|
||||||
|
Task<Tag?> GetByTagCodeAndServiceAsync(string tagCode, long serviceId);
|
||||||
Task<bool> ExistsInServiceAsync(long serviceId, string name);
|
Task<bool> ExistsInServiceAsync(long serviceId, string name);
|
||||||
|
Task<bool> TagCodeExistsInServiceAsync(long serviceId, string tagCode);
|
||||||
Task<(IReadOnlyList<Tag> Items, int TotalCount)> GetTagListAsync(
|
Task<(IReadOnlyList<Tag> Items, int TotalCount)> GetTagListAsync(
|
||||||
long? serviceId, string? keyword, int page, int size);
|
long? serviceId, string? keyword, int page, int size);
|
||||||
Task<int> CountByServiceAsync(long serviceId);
|
Task<int> CountByServiceAsync(long serviceId);
|
||||||
|
Task<IReadOnlyList<Tag>> GetByTagCodesAndServiceAsync(IEnumerable<string> tagCodes, long serviceId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1261
SPMS.Infrastructure/Migrations/20260302070717_AddTagCode.Designer.cs
generated
Normal file
1261
SPMS.Infrastructure/Migrations/20260302070717_AddTagCode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
SPMS.Infrastructure/Migrations/20260302070717_AddTagCode.cs
Normal file
53
SPMS.Infrastructure/Migrations/20260302070717_AddTagCode.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTagCode : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "TagCode",
|
||||||
|
table: "Tag",
|
||||||
|
type: "varchar(4)",
|
||||||
|
maxLength: 4,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
// 기존 태그에 4자리 랜덤 코드 할당 (서비스별 유니크)
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
UPDATE Tag
|
||||||
|
SET TagCode = CONCAT(
|
||||||
|
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', FLOOR(1 + RAND() * 62), 1),
|
||||||
|
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', FLOOR(1 + RAND() * 62), 1),
|
||||||
|
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', FLOOR(1 + RAND() * 62), 1),
|
||||||
|
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', FLOOR(1 + RAND() * 62), 1)
|
||||||
|
)
|
||||||
|
WHERE TagCode = '';
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tag_ServiceId_TagCode",
|
||||||
|
table: "Tag",
|
||||||
|
columns: new[] { "ServiceId", "TagCode" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Tag_ServiceId_TagCode",
|
||||||
|
table: "Tag");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TagCode",
|
||||||
|
table: "Tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -914,6 +914,11 @@ namespace SPMS.Infrastructure.Migrations
|
||||||
b.Property<long>("ServiceId")
|
b.Property<long>("ServiceId")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("TagCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("varchar(4)");
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
|
@ -924,6 +929,9 @@ namespace SPMS.Infrastructure.Migrations
|
||||||
b.HasIndex("ServiceId", "Name")
|
b.HasIndex("ServiceId", "Name")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ServiceId", "TagCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Tag", (string)null);
|
b.ToTable("Tag", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ public class TagConfiguration : IEntityTypeConfiguration<Tag>
|
||||||
builder.HasKey(e => e.Id);
|
builder.HasKey(e => e.Id);
|
||||||
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
builder.Property(e => e.TagCode).HasMaxLength(4).IsRequired();
|
||||||
builder.Property(e => e.Name).HasMaxLength(50).IsRequired();
|
builder.Property(e => e.Name).HasMaxLength(50).IsRequired();
|
||||||
builder.Property(e => e.Description).HasMaxLength(200);
|
builder.Property(e => e.Description).HasMaxLength(200);
|
||||||
builder.Property(e => e.CreatedAt).IsRequired();
|
builder.Property(e => e.CreatedAt).IsRequired();
|
||||||
|
|
@ -30,5 +31,6 @@ public class TagConfiguration : IEntityTypeConfiguration<Tag>
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
builder.HasIndex(e => new { e.ServiceId, e.Name }).IsUnique();
|
builder.HasIndex(e => new { e.ServiceId, e.Name }).IsUnique();
|
||||||
|
builder.HasIndex(e => new { e.ServiceId, e.TagCode }).IsUnique();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
|
||||||
public async Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
|
public async Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
|
||||||
long? serviceId, int page, int size,
|
long? serviceId, int page, int size,
|
||||||
Platform? platform = null, bool? pushAgreed = null,
|
Platform? platform = null, bool? pushAgreed = null,
|
||||||
bool? isActive = null, List<int>? tags = null,
|
bool? isActive = null, List<long>? tagIds = null,
|
||||||
string? keyword = null, bool? marketingAgreed = null)
|
string? keyword = null, bool? marketingAgreed = null)
|
||||||
{
|
{
|
||||||
IQueryable<Device> query = _dbSet.Include(d => d.Service);
|
IQueryable<Device> query = _dbSet.Include(d => d.Service);
|
||||||
|
|
@ -63,11 +63,11 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
|
||||||
query = query.Where(d => d.DeviceToken.Contains(trimmed));
|
query = query.Where(d => d.DeviceToken.Contains(trimmed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags != null && tags.Count > 0)
|
if (tagIds != null && tagIds.Count > 0)
|
||||||
{
|
{
|
||||||
foreach (var tag in tags)
|
foreach (var tagId in tagIds)
|
||||||
{
|
{
|
||||||
var tagStr = tag.ToString();
|
var tagStr = tagId.ToString();
|
||||||
query = query.Where(d => d.Tags != null && EF.Functions.Like(d.Tags, $"%{tagStr}%"));
|
query = query.Where(d => d.Tags != null && EF.Functions.Like(d.Tags, $"%{tagStr}%"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
|
||||||
public async Task<IReadOnlyList<Device>> GetAllFilteredAsync(
|
public async Task<IReadOnlyList<Device>> GetAllFilteredAsync(
|
||||||
long? serviceId,
|
long? serviceId,
|
||||||
Platform? platform = null, bool? pushAgreed = null,
|
Platform? platform = null, bool? pushAgreed = null,
|
||||||
bool? isActive = null, List<int>? tags = null,
|
bool? isActive = null, List<long>? tagIds = null,
|
||||||
string? keyword = null, bool? marketingAgreed = null)
|
string? keyword = null, bool? marketingAgreed = null)
|
||||||
{
|
{
|
||||||
IQueryable<Device> query = _dbSet.Include(d => d.Service);
|
IQueryable<Device> query = _dbSet.Include(d => d.Service);
|
||||||
|
|
@ -115,11 +115,11 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
|
||||||
query = query.Where(d => d.DeviceToken.Contains(trimmed));
|
query = query.Where(d => d.DeviceToken.Contains(trimmed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags != null && tags.Count > 0)
|
if (tagIds != null && tagIds.Count > 0)
|
||||||
{
|
{
|
||||||
foreach (var tag in tags)
|
foreach (var tagId in tagIds)
|
||||||
{
|
{
|
||||||
var tagStr = tag.ToString();
|
var tagStr = tagId.ToString();
|
||||||
query = query.Where(d => d.Tags != null && EF.Functions.Like(d.Tags, $"%{tagStr}%"));
|
query = query.Where(d => d.Tags != null && EF.Functions.Like(d.Tags, $"%{tagStr}%"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,17 @@ public class TagRepository : Repository<Tag>, ITagRepository
|
||||||
.Include(t => t.Service)
|
.Include(t => t.Service)
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
.FirstOrDefaultAsync(t => t.Id == id);
|
||||||
|
|
||||||
|
public async Task<Tag?> GetByTagCodeAndServiceAsync(string tagCode, long serviceId)
|
||||||
|
=> await _dbSet
|
||||||
|
.Include(t => t.Service)
|
||||||
|
.FirstOrDefaultAsync(t => t.TagCode == tagCode && t.ServiceId == serviceId);
|
||||||
|
|
||||||
public async Task<bool> ExistsInServiceAsync(long serviceId, string name)
|
public async Task<bool> ExistsInServiceAsync(long serviceId, string name)
|
||||||
=> await _dbSet.AnyAsync(t => t.ServiceId == serviceId && t.Name == name);
|
=> await _dbSet.AnyAsync(t => t.ServiceId == serviceId && t.Name == name);
|
||||||
|
|
||||||
|
public async Task<bool> TagCodeExistsInServiceAsync(long serviceId, string tagCode)
|
||||||
|
=> await _dbSet.AnyAsync(t => t.ServiceId == serviceId && t.TagCode == tagCode);
|
||||||
|
|
||||||
public async Task<(IReadOnlyList<Tag> Items, int TotalCount)> GetTagListAsync(
|
public async Task<(IReadOnlyList<Tag> Items, int TotalCount)> GetTagListAsync(
|
||||||
long? serviceId, string? keyword, int page, int size)
|
long? serviceId, string? keyword, int page, int size)
|
||||||
{
|
{
|
||||||
|
|
@ -43,4 +51,12 @@ public class TagRepository : Repository<Tag>, ITagRepository
|
||||||
|
|
||||||
public async Task<int> CountByServiceAsync(long serviceId)
|
public async Task<int> CountByServiceAsync(long serviceId)
|
||||||
=> await _dbSet.CountAsync(t => t.ServiceId == serviceId);
|
=> await _dbSet.CountAsync(t => t.ServiceId == serviceId);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Tag>> GetByTagCodesAndServiceAsync(IEnumerable<string> tagCodes, long serviceId)
|
||||||
|
{
|
||||||
|
var codeList = tagCodes.ToList();
|
||||||
|
return await _dbSet
|
||||||
|
.Where(t => t.ServiceId == serviceId && codeList.Contains(t.TagCode))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ public class PushWorker : BackgroundService
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<IServiceRepository>();
|
var serviceRepo = scope.ServiceProvider.GetRequiredService<IServiceRepository>();
|
||||||
var deviceRepo = scope.ServiceProvider.GetRequiredService<IDeviceRepository>();
|
var deviceRepo = scope.ServiceProvider.GetRequiredService<IDeviceRepository>();
|
||||||
|
var tagRepo = scope.ServiceProvider.GetRequiredService<ITagRepository>();
|
||||||
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
|
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
|
||||||
|
|
||||||
// 서비스 조회
|
// 서비스 조회
|
||||||
|
|
@ -161,7 +162,7 @@ public class PushWorker : BackgroundService
|
||||||
}
|
}
|
||||||
|
|
||||||
// [2] send_type별 대상 Device 조회
|
// [2] send_type별 대상 Device 조회
|
||||||
var devices = await ResolveDevicesAsync(deviceRepo, pushMessage, ct);
|
var devices = await ResolveDevicesAsync(deviceRepo, tagRepo, pushMessage, ct);
|
||||||
if (devices.Count == 0)
|
if (devices.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("발송 대상 없음: requestId={RequestId}", pushMessage.RequestId);
|
_logger.LogInformation("발송 대상 없음: requestId={RequestId}", pushMessage.RequestId);
|
||||||
|
|
@ -296,7 +297,7 @@ public class PushWorker : BackgroundService
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Device>> ResolveDevicesAsync(
|
private async Task<List<Device>> ResolveDevicesAsync(
|
||||||
IDeviceRepository deviceRepo, PushMessageDto message, CancellationToken ct)
|
IDeviceRepository deviceRepo, ITagRepository tagRepo, PushMessageDto message, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var sendType = message.SendType.ToLowerInvariant();
|
var sendType = message.SendType.ToLowerInvariant();
|
||||||
|
|
||||||
|
|
@ -346,15 +347,21 @@ public class PushWorker : BackgroundService
|
||||||
|
|
||||||
case "group":
|
case "group":
|
||||||
{
|
{
|
||||||
var tags = ParseTags(message.Target);
|
// tag_code(string) 목록 파싱
|
||||||
if (tags.Count == 0) return [];
|
var tagCodes = ParseTags(message.Target);
|
||||||
|
if (tagCodes.Count == 0) return [];
|
||||||
|
|
||||||
|
// tag_code → tag_id 변환
|
||||||
|
var tags = await tagRepo.GetByTagCodesAndServiceAsync(tagCodes, message.ServiceId);
|
||||||
|
var tagIdStrings = tags.Select(t => t.Id.ToString()).ToHashSet();
|
||||||
|
if (tagIdStrings.Count == 0) return [];
|
||||||
|
|
||||||
var allDevices = await deviceRepo.FindAsync(
|
var allDevices = await deviceRepo.FindAsync(
|
||||||
d => d.ServiceId == message.ServiceId && d.IsActive && d.PushAgreed);
|
d => d.ServiceId == message.ServiceId && d.IsActive && d.PushAgreed);
|
||||||
|
|
||||||
return allDevices
|
return allDevices
|
||||||
.Where(d => !string.IsNullOrEmpty(d.Tags) &&
|
.Where(d => !string.IsNullOrEmpty(d.Tags) &&
|
||||||
tags.Any(tag => d.Tags.Contains(tag)))
|
tagIdStrings.Any(tagIdStr => d.Tags.Contains(tagIdStr)))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,9 +403,23 @@ public class PushWorker : BackgroundService
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (target.Value.Value.ValueKind == JsonValueKind.Array)
|
var element = target.Value.Value;
|
||||||
|
|
||||||
|
// { tags: [...], match: "or" } 형태
|
||||||
|
if (element.ValueKind == JsonValueKind.Object &&
|
||||||
|
element.TryGetProperty("tags", out var tagsElement) &&
|
||||||
|
tagsElement.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
return target.Value.Value.EnumerateArray()
|
return tagsElement.EnumerateArray()
|
||||||
|
.Where(e => e.ValueKind == JsonValueKind.String)
|
||||||
|
.Select(e => e.GetString()!)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 직접 배열 형태 (호환)
|
||||||
|
if (element.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return element.EnumerateArray()
|
||||||
.Where(e => e.ValueKind == JsonValueKind.String)
|
.Where(e => e.ValueKind == JsonValueKind.String)
|
||||||
.Select(e => e.GetString()!)
|
.Select(e => e.GetString()!)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user