using System.Text.RegularExpressions; namespace bitforum.Services { public enum UploadTarget { Editor, // 에디터 Upload // 직접 첨부 } public enum UploadFolder { Basic, // 기본 설정 Document, // 문서 Faq, // 자주 묻는 질문 Popup, // 팝업 Banner, // 배너 Grade, // 회원 등급 Template, // 알림 발송 양식 Member // 회원 사진/아이콘 } public interface IFileUploadService { // 에디터의 모든 파일 삭제 public Task CleanUpEditorImagesAsync(string? content); // 에디터에 첨부된 이미지를 저장 public Task UploadEditorAsync(string? newContent, string? oldContent, UploadFolder folder, int? ID = null); // 직접 첨부한 이미지 저장 public Task UploadImageAsync(IFormFile? file, UploadFolder folder, int? ID = null); // 물리적 파일 저장 public Task UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadTarget target, UploadFolder folder, int? ID = null); // 파일 삭제 public bool RemoveFile(string? filePath); } public class FileUploadService : IFileUploadService { private readonly ILogger _logger; private readonly IWebHostEnvironment _environment; public FileUploadService(ILogger logger, IWebHostEnvironment environment) { _logger = logger; _environment = environment; } // 절대 경로 생성 private string GetAbosolutePath(UploadTarget target, UploadFolder folder, int? ID = null) { return Path.Combine(_environment.WebRootPath, GetRelativePath(target, folder, ID)); } // 상대 경로 생성 private string GetRelativePath(UploadTarget target, UploadFolder folder, int? ID = null) { string t = target switch { UploadTarget.Editor => "editor", UploadTarget.Upload => "upload", _ => throw new ArgumentException("유효하지 않은 `Upload target` 입니다.") }; string f = folder switch { UploadFolder.Basic => "basic", UploadFolder.Document => "document", UploadFolder.Faq => "faq", UploadFolder.Popup => "popup", UploadFolder.Banner => "banner", UploadFolder.Grade => "grade", UploadFolder.Template => "template", UploadFolder.Member => "member", _ => throw new ArgumentException("유효하지 않은 `Upload folder` 입니다.") }; return Path.Combine(t, f, ID?.ToString() ?? string.Empty); } private async Task CleanUpUnusedImagesAsync(string uploadPath, HashSet usedImages) { try { var files = Directory.GetFiles(uploadPath); if (files.Length == 0) { Console.WriteLine("삭제할 미사용 이미지가 없습니다."); return; } var deleteTasks = files.Select(async file => { var path = $"/{Path.GetRelativePath(_environment.WebRootPath, file).Replace("\\", "/")}"; if (!usedImages.Contains(path)) { try { await Task.Run(() => File.Delete(file)); // 비동기 파일 삭제 Console.WriteLine($"이미지 삭제: {path}"); } catch (Exception ex) { Console.WriteLine($"이미지 삭제 실패: {path}, 오류: {ex.Message}"); } } }); await Task.WhenAll(deleteTasks); // 모든 파일 삭제를 병렬 실행 } catch (Exception e) { Console.WriteLine($"미사용 파일 정리 실패: {e.Message}"); } } private async Task CleanUpOldImagesAsync(HashSet usedImages, HashSet oldImages) { try { var unUsedImages = oldImages.Except(usedImages).ToList(); // 삭제할 이미지 찾기 if (unUsedImages.Count > 0) { var deleteTasks = unUsedImages.Select(async img => { var path = Path.Combine(_environment.WebRootPath, img.TrimStart('/')); if (File.Exists(path)) { try { await Task.Run(() => File.Delete(path)); // 비동기 삭제 Console.WriteLine($"이미지 삭제: {path}"); } catch (Exception ex) { Console.WriteLine($"이미지 삭제 실패: {path}, 오류: {ex.Message}"); } } }); await Task.WhenAll(deleteTasks); // 모든 삭제 작업 비동기 실행 } } catch (Exception e) { Console.WriteLine($"미사용 파일 정리 실패: {e.Message}"); } } public async Task CleanUpEditorImagesAsync(string? content) { string? message = null; if (string.IsNullOrEmpty(content)) { message = "삭제할 내용이 비어있습니다."; Console.WriteLine(message); _logger.LogWarning(message); return; } // 태그에서 이미지 URL 목록 가져오기 var matches = Regex.Matches(content, @"]+src=""(?[^""]+)""[^>]*>"); if (matches.Count == 0) { message = "삭제할 이미지 파일이 없습니다."; Console.WriteLine(message); _logger.LogInformation(message); return; } var deleteTasks = matches.Select(async img => { string filePath = Path.Combine(_environment.WebRootPath, img.Groups["src"].Value.TrimStart('/')); if (File.Exists(filePath)) { try { await Task.Run(() => File.Delete(filePath)); // 비동기 삭제 message = $"파일 삭제 완료: {filePath}"; Console.WriteLine(message); _logger.LogInformation(message); } catch (Exception ex) { message = $"파일 삭제 실패: {filePath}, 오류: {ex.Message}"; Console.WriteLine(message); _logger.LogError(message); } } }); await Task.WhenAll(deleteTasks); message = $"총 {matches.Count}개의 파일이 삭제됨"; Console.WriteLine(message); _logger.LogInformation(message); } public async Task UploadEditorAsync(string? newContent, string? oldContent, UploadFolder folder, int? ID = null) { string path = GetRelativePath(UploadTarget.Editor, folder, ID).Replace("\\", "/"); string uploadPath = GetAbosolutePath(UploadTarget.Editor, folder, ID); var usedImages = new HashSet(); // 새로운 content에서 사용된 이미지 목록 var oldImages = new HashSet(); // 기존 content에서 사용된 이미지 목록 if (!Directory.Exists(uploadPath)) { Directory.CreateDirectory(uploadPath); } // oldContent에서 기존 이미지 URL 목록 if (!string.IsNullOrEmpty(oldContent)) { var oldMatches = Regex.Matches(oldContent, @"]+src=""(?[^""]+)""[^>]*>"); foreach (Match img in oldMatches) { oldImages.Add(img.Groups["src"].Value); } } // 태그에서 현재 사용 중인 이미지 URL 목록 가져오기 if (!string.IsNullOrEmpty(newContent)) { var matches = Regex.Matches(newContent, @"]+src=""(?[^""]+)""[^>]*>"); foreach (Match img in matches) { var src = img.Groups["src"].Value; if (src.StartsWith("data:image/")) { try { var match = Regex.Match(src, @"^data:image/(?.+?);base64,(?.+)$"); if (!match.Success) { throw new Exception("Base64 이미지가 아닙니다."); } var extension = match.Groups["extension"].Value; var data = match.Groups["data"].Value; // 확장자와 파일명 생성 var fileName = $"{Guid.NewGuid()}.{extension}"; var filePath = Path.Combine(uploadPath, fileName); var webPath = $"/{path}/{fileName}"; await File.WriteAllBytesAsync(filePath, Convert.FromBase64String(data)); // Base64 데이터 디코딩 및 파일 저장 newContent = newContent.Replace(src, webPath); // HTML에서 Base64 데이터를 실제 이미지 URL로 대체 usedImages.Add(webPath); // 사용한 이미지 목록에 추가 } catch (Exception e) { _logger.LogWarning($"파일을 생성할 수 없음: {e.Message}"); Console.WriteLine(e.Message); continue; } } else if (src.StartsWith($"/{path}")) { usedImages.Add(src); } } } // 사용하지 않는 이미지 파일 삭제 await CleanUpOldImagesAsync(usedImages, oldImages); if (ID.HasValue) { await CleanUpUnusedImagesAsync(uploadPath, usedImages); } return newContent; } public async Task UploadImageAsync(IFormFile? file, UploadFolder folder, int? ID = null) { return await UploadFileAsync(file, [".jpg", ".jpeg", ".png", ".gif"], UploadTarget.Upload, folder, ID); } public async Task UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadTarget target, UploadFolder folder, int ?ID = null) { if (file == null || file.Length == 0) { return null; } var extension = Path.GetExtension(file.FileName).ToLower(); if (!Array.Exists(allowedExtensions, ext => ext == extension)) { throw new ArgumentException("허용되지 않는 파일 형식입니다."); } var uploadPath = GetAbosolutePath(target, folder, ID); if (!Directory.Exists(uploadPath)) { Directory.CreateDirectory(uploadPath); } var fileName = $"{Guid.NewGuid()}{extension}"; var filePath = Path.Combine(uploadPath, fileName); var webPath = $"/{GetRelativePath(target, folder, ID)}/{fileName}".Replace("\\", "/"); // 파일 저장 using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } return webPath; } public bool RemoveFile(string? filePath) { if (string.IsNullOrEmpty(filePath)) { _logger.LogWarning("삭제 요청된 파일 경로가 비어 있습니다."); return false; } var fullPath = Path.Combine(_environment.WebRootPath, filePath.TrimStart('/')); if (File.Exists(fullPath)) { try { File.Delete(fullPath); _logger.LogInformation($"파일 삭제 성공: {fullPath}"); return true; } catch (Exception e) { _logger.LogError($"파일 삭제 실패: {fullPath}, 오류: {e.Message}"); return false; } } _logger.LogWarning($"파일이 존재하지 않음: {fullPath}"); return false; } } }