improvement: 기기 엑셀 내보내기 API 추가 (#241) #242

Merged
seonkyu.kim merged 1 commits from improvement/#241-device-export into develop 2026-02-25 08:19:57 +00:00
6 changed files with 142 additions and 0 deletions

View File

@ -92,6 +92,17 @@ public class DeviceController : ControllerBase
return Ok(ApiResponse<DeviceListResponseDto>.Success(result)); return Ok(ApiResponse<DeviceListResponseDto>.Success(result));
} }
[HttpPost("export")]
[Authorize]
[SwaggerOperation(Summary = "기기 엑셀 내보내기", Description = "목록 필터와 동일 조건으로 기기 목록을 엑셀(.xlsx)로 내보냅니다. JWT 인증 필요.")]
public async Task<IActionResult> ExportAsync([FromBody] DeviceExportRequestDto request)
{
var serviceId = GetOptionalServiceId();
var fileBytes = await _deviceService.ExportAsync(serviceId, request);
var fileName = $"device_export_{DateTime.UtcNow:yyyyMMdd}.xlsx";
return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
}
private long GetServiceId() private long GetServiceId()
{ {
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId) if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)

View File

@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace SPMS.Application.DTOs.Device;
public class DeviceExportRequestDto
{
[JsonPropertyName("platform")]
public string? Platform { get; set; }
[JsonPropertyName("push_agreed")]
public bool? PushAgreed { get; set; }
[JsonPropertyName("tags")]
public List<int>? Tags { get; set; }
[JsonPropertyName("is_active")]
public bool? IsActive { get; set; }
[JsonPropertyName("keyword")]
public string? Keyword { get; set; }
[JsonPropertyName("marketing_agreed")]
public bool? MarketingAgreed { get; set; }
}

View File

@ -10,6 +10,7 @@ public interface IDeviceService
Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request); Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request);
Task AdminDeleteAsync(long deviceId); Task AdminDeleteAsync(long deviceId);
Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request); Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request);
Task<byte[]> ExportAsync(long? serviceId, DeviceExportRequestDto request);
Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request); Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request);
Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request); Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request);
} }

View File

@ -174,6 +174,61 @@ public class DeviceService : IDeviceService
}; };
} }
public async Task<byte[]> ExportAsync(long? serviceId, DeviceExportRequestDto request)
{
Platform? platform = null;
if (!string.IsNullOrWhiteSpace(request.Platform))
platform = ParsePlatform(request.Platform);
var items = await _deviceRepository.GetAllFilteredAsync(
serviceId, platform, request.PushAgreed, request.IsActive,
request.Tags, 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) public async Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request)
{ {
var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId); var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId);

View File

@ -14,4 +14,9 @@ public interface IDeviceRepository : IRepository<Device>
Platform? platform = null, bool? pushAgreed = null, Platform? platform = null, bool? pushAgreed = null,
bool? isActive = null, List<int>? tags = null, bool? isActive = null, List<int>? tags = null,
string? keyword = null, bool? marketingAgreed = null); string? keyword = null, bool? marketingAgreed = null);
Task<IReadOnlyList<Device>> GetAllFilteredAsync(
long? serviceId,
Platform? platform = null, bool? pushAgreed = null,
bool? isActive = null, List<int>? tags = null,
string? keyword = null, bool? marketingAgreed = null);
} }

View File

@ -82,4 +82,50 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
return (items, totalCount); return (items, totalCount);
} }
public async Task<IReadOnlyList<Device>> GetAllFilteredAsync(
long? serviceId,
Platform? platform = null, bool? pushAgreed = null,
bool? isActive = null, List<int>? tags = null,
string? keyword = null, bool? marketingAgreed = null)
{
IQueryable<Device> query = _dbSet.Include(d => d.Service);
if (serviceId.HasValue)
query = query.Where(d => d.ServiceId == serviceId.Value);
if (platform.HasValue)
query = query.Where(d => d.Platform == platform.Value);
if (pushAgreed.HasValue)
query = query.Where(d => d.PushAgreed == pushAgreed.Value);
if (isActive.HasValue)
query = query.Where(d => d.IsActive == isActive.Value);
if (marketingAgreed.HasValue)
query = query.Where(d => d.MarketingAgreed == marketingAgreed.Value);
if (!string.IsNullOrWhiteSpace(keyword))
{
var trimmed = keyword.Trim();
if (long.TryParse(trimmed, out var deviceId))
query = query.Where(d => d.Id == deviceId || d.DeviceToken.Contains(trimmed));
else
query = query.Where(d => d.DeviceToken.Contains(trimmed));
}
if (tags != null && tags.Count > 0)
{
foreach (var tag in tags)
{
var tagStr = tag.ToString();
query = query.Where(d => d.Tags != null && EF.Functions.Like(d.Tags, $"%{tagStr}%"));
}
}
return await query
.OrderByDescending(d => d.CreatedAt)
.ToListAsync();
}
} }