using SPMS.Application.DTOs.File; using SPMS.Application.DTOs.Notice; using SPMS.Application.Interfaces; using SPMS.Domain.Common; using SPMS.Domain.Entities; using SPMS.Domain.Exceptions; using SPMS.Domain.Interfaces; namespace SPMS.Application.Services; public class FileService : IFileService { private readonly IFileRepository _fileRepository; private readonly IFileStorageService _fileStorageService; private readonly IServiceRepository _serviceRepository; private readonly IAdminRepository _adminRepository; private readonly IUnitOfWork _unitOfWork; private static readonly HashSet AllowedImageExtensions = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".gif" }; private static readonly HashSet AllowedCsvExtensions = new(StringComparer.OrdinalIgnoreCase) { ".csv" }; private const long MaxImageSize = 5 * 1024 * 1024; // 5MB private const long MaxCsvSize = 50 * 1024 * 1024; // 50MB public FileService( IFileRepository fileRepository, IFileStorageService fileStorageService, IServiceRepository serviceRepository, IAdminRepository adminRepository, IUnitOfWork unitOfWork) { _fileRepository = fileRepository; _fileStorageService = fileStorageService; _serviceRepository = serviceRepository; _adminRepository = adminRepository; _unitOfWork = unitOfWork; } public async Task UploadAsync( long serviceId, long adminId, Stream fileStream, string fileName, long fileSize, string fileType) { ValidateFileType(fileType, fileName, fileSize); var extension = Path.GetExtension(fileName).ToLowerInvariant(); var mimeType = GetMimeType(extension); var savedPath = await _fileStorageService.SaveAsync(serviceId, fileName, fileStream); var entity = new FileEntity { ServiceId = serviceId, FileName = fileName, FilePath = savedPath, FileSize = fileSize, FileType = fileType.ToLowerInvariant(), MimeType = mimeType, CreatedAt = DateTime.UtcNow, CreatedBy = adminId }; await _fileRepository.AddAsync(entity); await _unitOfWork.SaveChangesAsync(); return new FileUploadResponseDto { FileId = entity.Id, FileName = entity.FileName, FileUrl = _fileStorageService.GetFileUrl(entity.FilePath), FileSize = entity.FileSize, FileType = entity.FileType, CreatedAt = entity.CreatedAt }; } public async Task GetInfoAsync(long serviceId, long fileId) { var file = await _fileRepository.GetByIdAndServiceAsync(fileId, serviceId); if (file == null || file.IsDeleted) throw new SpmsException(ErrorCodes.FileNotFound, "존재하지 않는 파일입니다.", 404); var service = await _serviceRepository.GetByIdAsync(file.ServiceId); var admin = await _adminRepository.GetByIdAsync(file.CreatedBy); return new FileInfoResponseDto { FileId = file.Id, ServiceCode = service?.ServiceCode ?? string.Empty, FileName = file.FileName, FileUrl = _fileStorageService.GetFileUrl(file.FilePath), FileSize = file.FileSize, FileType = file.FileType, UploadedBy = admin?.Email ?? string.Empty, CreatedAt = file.CreatedAt }; } public async Task GetListAsync(long serviceId, FileListRequestDto request) { var (items, totalCount) = await _fileRepository.GetPagedByServiceAsync( serviceId, request.Page, request.Size, request.FileType); var totalPages = (int)Math.Ceiling((double)totalCount / request.Size); return new FileListResponseDto { Items = items.Select(f => new FileSummaryDto { FileId = f.Id, FileName = f.FileName, FileUrl = _fileStorageService.GetFileUrl(f.FilePath), FileSize = f.FileSize, FileType = f.FileType, CreatedAt = f.CreatedAt }).ToList(), Pagination = new PaginationDto { Page = request.Page, Size = request.Size, TotalCount = totalCount, TotalPages = totalPages } }; } public async Task DeleteAsync(long serviceId, long fileId) { var file = await _fileRepository.GetByIdAndServiceAsync(fileId, serviceId); if (file == null || file.IsDeleted) throw new SpmsException(ErrorCodes.FileNotFound, "존재하지 않는 파일입니다.", 404); file.IsDeleted = true; file.DeletedAt = DateTime.UtcNow; _fileRepository.Update(file); await _unitOfWork.SaveChangesAsync(); } private static void ValidateFileType(string fileType, string fileName, long fileSize) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); switch (fileType.ToLowerInvariant()) { case "image": if (!AllowedImageExtensions.Contains(extension)) throw new SpmsException(ErrorCodes.FileTypeNotAllowed, "허용되지 않는 이미지 형식입니다. (jpg, png, gif만 가능)", 400); if (fileSize > MaxImageSize) throw new SpmsException(ErrorCodes.FileSizeExceeded, "이미지 파일은 5MB를 초과할 수 없습니다.", 400); break; case "csv": if (!AllowedCsvExtensions.Contains(extension)) throw new SpmsException(ErrorCodes.FileTypeNotAllowed, "허용되지 않는 파일 형식입니다. (csv만 가능)", 400); if (fileSize > MaxCsvSize) throw new SpmsException(ErrorCodes.FileSizeExceeded, "CSV 파일은 50MB를 초과할 수 없습니다.", 400); break; default: throw new SpmsException(ErrorCodes.FileTypeNotAllowed, "유효하지 않은 파일 타입입니다. (image, csv만 가능)", 400); } } private static string GetMimeType(string extension) { return extension switch { ".jpg" or ".jpeg" => "image/jpeg", ".png" => "image/png", ".gif" => "image/gif", ".csv" => "text/csv", _ => "application/octet-stream" }; } }