ASP.NET Core 8 환경에서 Amazon S3와 CloudFront를 활용하여 안전하고 성능이 좋은 파일 CRUD 기능을 구현하는 방법을 설명해 드리겠습니다.
사용자 요청 → ASP.NET Core API → AWS SDK → S3 버킷
↑ ↓
└───── CloudFront ──┘
dotnet add package AWSSDK.S3
dotnet add package AWSSDK.CloudFront
dotnet add package AWSSDK.Extensions.NETCore.Setup
using Amazon.S3;
using Amazon.CloudFront;
using Amazon.Extensions.NETCore.Setup;
var builder = WebApplication.CreateBuilder(args);
// AWS 서비스 등록
builder.Services.AddAWSService<IAmazonS3>(new AWSOptions
{
Region = Amazon.RegionEndpoint.APNortheast2 // 서울 리전
});
builder.Services.AddAWSService<IAmazonCloudFront>(new AWSOptions
{
Region = Amazon.RegionEndpoint.APNortheast2
});
// 파일 서비스 등록
builder.Services.AddScoped<IFileService, S3FileService>();
// API 컨트롤러 등록
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
public interface IFileService
{
Task<FileUploadResult> UploadFileAsync(IFormFile file, string directory, CancellationToken cancellationToken = default);
Task<byte[]> DownloadFileAsync(string key, CancellationToken cancellationToken = default);
Task<string> GetPresignedUrlAsync(string key, TimeSpan expiry, CancellationToken cancellationToken = default);
Task<string> GetCloudFrontUrlAsync(string key, DateTime expiryTime, CancellationToken cancellationToken = default);
Task<bool> DeleteFileAsync(string key, CancellationToken cancellationToken = default);
Task<bool> UpdateFileAsync(string key, IFormFile newFile, CancellationToken cancellationToken = default);
}
public class FileUploadResult
{
public bool Success { get; set; }
public string Key { get; set; }
public string Url { get; set; }
public string ErrorMessage { get; set; }
}
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.CloudFront;
using Amazon.CloudFront.Model;
using Microsoft.Extensions.Configuration;
using System.Security.Cryptography;
public class S3FileService : IFileService
{
private readonly IAmazonS3 _s3Client;
private readonly IAmazonCloudFront _cloudFrontClient;
private readonly string _bucketName;
private readonly string _cloudFrontDomain;
private readonly string _cloudFrontKeyPairId;
private readonly string _cloudFrontPrivateKey;
public S3FileService(
IAmazonS3 s3Client,
IAmazonCloudFront cloudFrontClient,
IConfiguration configuration)
{
_s3Client = s3Client;
_cloudFrontClient = cloudFrontClient;
_bucketName = configuration["AWS:S3:BucketName"];
_cloudFrontDomain = configuration["AWS:CloudFront:Domain"];
_cloudFrontKeyPairId = configuration["AWS:CloudFront:KeyPairId"];
_cloudFrontPrivateKey = configuration["AWS:CloudFront:PrivateKey"];
}
public async Task<FileUploadResult> UploadFileAsync(IFormFile file, string directory, CancellationToken cancellationToken = default)
{
try
{
// 파일명 충돌 방지를 위한 유니크 키 생성
var fileName = Path.GetFileName(file.FileName);
var fileExtension = Path.GetExtension(fileName);
var uniqueFileName = $"{Guid.NewGuid()}{fileExtension}";
var key = string.IsNullOrEmpty(directory)
? uniqueFileName
: $"{directory.TrimEnd('/')}/{uniqueFileName}";
// 메타데이터 설정
var metadata = new Dictionary<string, string>
{
{ "original-filename", fileName },
{ "content-type", file.ContentType },
{ "upload-date", DateTime.UtcNow.ToString("o") }
};
using var fileStream = file.OpenReadStream();
var putObjectRequest = new PutObjectRequest
{
BucketName = _bucketName,
Key = key,
InputStream = fileStream,
ContentType = file.ContentType,
Metadata = metadata
};
await _s3Client.PutObjectAsync(putObjectRequest, cancellationToken);
// CloudFront URL 생성
var cloudFrontUrl = $"https://{_cloudFrontDomain}/{key}";
return new FileUploadResult
{
Success = true,
Key = key,
Url = cloudFrontUrl
};
}
catch (Exception ex)
{
return new FileUploadResult
{
Success = false,
ErrorMessage = ex.Message
};
}
}
public async Task<byte[]> DownloadFileAsync(string key, CancellationToken cancellationToken = default)
{
try
{
var request = new GetObjectRequest
{
BucketName = _bucketName,
Key = key
};
using var response = await _s3Client.GetObjectAsync(request, cancellationToken);
using var responseStream = response.ResponseStream;
using var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream, cancellationToken);
return memoryStream.ToArray();
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
throw new FileNotFoundException($"File with key '{key}' not found", ex);
}
}
public async Task<string> GetPresignedUrlAsync(string key, TimeSpan expiry, CancellationToken cancellationToken = default)
{
var request = new GetPreSignedUrlRequest
{
BucketName = _bucketName,
Key = key,
Expires = DateTime.UtcNow.Add(expiry)
};
return await Task.FromResult(_s3Client.GetPreSignedURL(request));
}
public async Task<string> GetCloudFrontUrlAsync(string key, DateTime expiryTime, CancellationToken cancellationToken = default)
{
// CloudFront 서명된 URL을 생성
var resourceUrl = $"https://{_cloudFrontDomain}/{key}";
var policy = CreateCustomPolicy(resourceUrl, expiryTime);
// 서명 생성
string signature = CreatePolicySignature(policy);
// 서명된 URL 생성
var signedUrl = $"{resourceUrl}?Policy={UrlSafeBase64Encode(policy)}&Signature={signature}&Key-Pair-Id={_cloudFrontKeyPairId}";
return await Task.FromResult(signedUrl);
}
public async Task<bool> DeleteFileAsync(string key, CancellationToken cancellationToken = default)
{
try
{
var deleteRequest = new DeleteObjectRequest
{
BucketName = _bucketName,
Key = key
};
var response = await _s3Client.DeleteObjectAsync(deleteRequest, cancellationToken);
return response.HttpStatusCode == System.Net.HttpStatusCode.NoContent;
}
catch
{
return false;
}
}
public async Task<bool> UpdateFileAsync(string key, IFormFile newFile, CancellationToken cancellationToken = default)
{
try
{
// 기존 파일이 있는지 확인
var existingObject = await _s3Client.GetObjectMetadataAsync(
new GetObjectMetadataRequest
{
BucketName = _bucketName,
Key = key
},
cancellationToken);
// 파일이 존재하면 덮어쓰기
using var fileStream = newFile.OpenReadStream();
var putObjectRequest = new PutObjectRequest
{
BucketName = _bucketName,
Key = key,
InputStream = fileStream,
ContentType = newFile.ContentType,
Metadata = new Dictionary<string, string>
{
{ "original-filename", Path.GetFileName(newFile.FileName) },
{ "content-type", newFile.ContentType },
{ "update-date", DateTime.UtcNow.ToString("o") }
}
};
var response = await _s3Client.PutObjectAsync(putObjectRequest, cancellationToken);
return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// 파일이 존재하지 않음
return false;
}
}
#region CloudFront 서명 유틸리티
private string CreateCustomPolicy(string resourceUrl, DateTime expiryTime)
{
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var epochSeconds = (long)(expiryTime - epoch).TotalSeconds;
return $@"{{
""Statement"": [
{{
""Resource"": ""{resourceUrl}"",
""Condition"": {{
""DateLessThan"": {{
""AWS:EpochTime"": {epochSeconds}
}}
}}
}}
]
}}";
}
private string CreatePolicySignature(string policy)
{
// 실제 구현에서는 CloudFront 개인 키를 로드하고 서명을 생성해야 함
// 여기서는 단순화된 예제 제공
var privateKeyBytes = Convert.FromBase64String(_cloudFrontPrivateKey);
using var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
var policyBytes = System.Text.Encoding.UTF8.GetBytes(policy);
var signatureBytes = rsa.SignData(policyBytes, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
return UrlSafeBase64Encode(Convert.ToBase64String(signatureBytes));
}
private string UrlSafeBase64Encode(string input)
{
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(input))
.Replace('+', '-')
.Replace('=', '_')
.Replace('/', '~');
}
#endregion
}
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly IFileService _fileService;
private readonly ILogger<FilesController> _logger;
public FilesController(IFileService fileService, ILogger<FilesController> logger)
{
_fileService = fileService;
_logger = logger;
}
[HttpPost("upload")]
[Authorize]
[RequestSizeLimit(20 * 1024 * 1024)] // 20MB 제한
public async Task<IActionResult> UploadFile(IFormFile file, [FromQuery] string directory = "")
{
if (file == null || file.Length == 0)
return BadRequest("파일이 제공되지 않았습니다.");
// 파일 유효성 검사
if (!IsValidFile(file))
return BadRequest("허용되지 않는 파일 형식입니다.");
// 사용자 ID 가져오기 (인증된 사용자)
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// 사용자별 디렉토리 설정 (선택사항)
var userDirectory = string.IsNullOrEmpty(directory)
? $"users/{userId}"
: $"users/{userId}/{directory}";
try
{
var result = await _fileService.UploadFileAsync(file, userDirectory);
if (!result.Success)
return StatusCode(500, result.ErrorMessage);
// 업로드 결과를 데이터베이스에 기록하는 로직을 여기에 추가
return Ok(new {
key = result.Key,
url = result.Url,
fileName = file.FileName,
contentType = file.ContentType,
size = file.Length
});
}
catch (Exception ex)
{
_logger.LogError(ex, "파일 업로드 중 오류가 발생했습니다.");
return StatusCode(500, "파일 업로드 중 오류가 발생했습니다.");
}
}
[HttpGet("download/{key}")]
[Authorize]
public async Task<IActionResult> DownloadFile(string key)
{
try
{
// 접근 권한 검증 (사용자가 해당 파일에 접근할 권한이 있는지)
if (!await UserHasAccessToFile(key))
return Forbid();
// 직접 파일 다운로드
var fileBytes = await _fileService.DownloadFileAsync(key);
// ContentType 추측 (실제 구현에서는 DB에서 조회하거나 메타데이터에서 가져올 수 있음)
var contentType = GetContentTypeFromFileName(key);
return File(fileBytes, contentType, Path.GetFileName(key));
}
catch (FileNotFoundException)
{
return NotFound("요청한 파일을 찾을 수 없습니다.");
}
catch (Exception ex)
{
_logger.LogError(ex, "파일 다운로드 중 오류가 발생했습니다.");
return StatusCode(500, "파일 다운로드 중 오류가 발생했습니다.");
}
}
[HttpGet("url/{key}")]
[Authorize]
public async Task<IActionResult> GetFileUrl(string key, [FromQuery] bool useCdn = true)
{
try
{
// 접근 권한 검증
if (!await UserHasAccessToFile(key))
return Forbid();
string url;
if (useCdn)
{
// CloudFront를 통한 서명된 URL (24시간 유효)
url = await _fileService.GetCloudFrontUrlAsync(key, DateTime.UtcNow.AddHours(24));
}
else
{
// S3 미리 서명된 URL (1시간 유효)
url = await _fileService.GetPresignedUrlAsync(key, TimeSpan.FromHours(1));
}
return Ok(new { url });
}
catch (Exception ex)
{
_logger.LogError(ex, "파일 URL 생성 중 오류가 발생했습니다.");
return StatusCode(500, "파일 URL 생성 중 오류가 발생했습니다.");
}
}
[HttpDelete("{key}")]
[Authorize]
public async Task<IActionResult> DeleteFile(string key)
{
try
{
// 접근 권한 검증
if (!await UserHasAccessToFile(key))
return Forbid();
var result = await _fileService.DeleteFileAsync(key);
if (!result)
return StatusCode(500, "파일 삭제 중 오류가 발생했습니다.");
// 데이터베이스에서 파일 기록 삭제하는 로직 추가
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, "파일 삭제 중 오류가 발생했습니다.");
return StatusCode(500, "파일 삭제 중 오류가 발생했습니다.");
}
}
[HttpPut("{key}")]
[Authorize]
[RequestSizeLimit(20 * 1024 * 1024)] // 20MB 제한
public async Task<IActionResult> UpdateFile(string key, IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("파일이 제공되지 않았습니다.");
// 파일 유효성 검사
if (!IsValidFile(file))
return BadRequest("허용되지 않는 파일 형식입니다.");
try
{
// 접근 권한 검증
if (!await UserHasAccessToFile(key))
return Forbid();
var result = await _fileService.UpdateFileAsync(key, file);
if (!result)
return NotFound("업데이트할 파일을 찾을 수 없습니다.");
// 데이터베이스 파일 기록 업데이트 로직 추가
return Ok(new {
message = "파일이 성공적으로 업데이트되었습니다.",
fileName = file.FileName,
contentType = file.ContentType,
size = file.Length
});
}
catch (Exception ex)
{
_logger.LogError(ex, "파일 업데이트 중 오류가 발생했습니다.");
return StatusCode(500, "파일 업데이트 중 오류가 발생했습니다.");
}
}
#region 헬퍼 메서드
// 실제 구현에서는 사용자와 파일 간의 관계를 DB에서 확인
private Task<bool> UserHasAccessToFile(string key)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// 파일 키가 사용자 디렉토리에 있는지 확인
var userDirectory = $"users/{userId}";
var hasAccess = key.StartsWith(userDirectory) || User.IsInRole("Admin");
return Task.FromResult(hasAccess);
}
private bool IsValidFile(IFormFile file)
{
// 허용된 파일 형식 및 확장자 검사
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (string.IsNullOrEmpty(extension) || !allowedExtensions.Contains(extension))
return false;
// 추가적인 파일 유효성 검사 (파일 시그니처 확인 등)
return true;
}
private string GetContentTypeFromFileName(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".pdf" => "application/pdf",
".doc" => "application/msword",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls" => "application/vnd.ms-excel",
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".ppt" => "application/vnd.ms-powerpoint",
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
_ => "application/octet-stream"
};
}
#endregion
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AWS": {
"Profile": "default",
"Region": "ap-northeast-2",
"S3": {
"BucketName": "your-bucket-name"
},
"CloudFront": {
"Domain": "your-distribution-id.cloudfront.net",
"KeyPairId": "your-key-pair-id",
"PrivateKey": "your-private-key-base64-encoded"
}
}
}
ASP.NET Core MVC에서 Razor 뷰와 JavaScript를 활용하여 파일 업로드, 다운로드, 목록 조회, 삭제 기능을 구현하겠습니다.
@{
ViewData["Title"] = "파일 관리";
}
<div class="container mt-4">
<h2>파일 관리</h2>
<!-- 파일 업로드 섹션 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">파일 업로드</h5>
</div>
<div class="card-body">
<form id="fileUploadForm" enctype="multipart/form-data">
<div class="mb-3">
<label for="directory" class="form-label">디렉토리 (선택사항)</label>
<input type="text" class="form-control" id="directory" name="directory" placeholder="예: 문서/2025">
</div>
<div class="mb-3">
<label for="fileInput" class="form-label">파일 선택</label>
<input type="file" class="form-control" id="fileInput" name="file" required>
<div class="form-text">최대 20MB까지 업로드 가능. 지원 파일 형식: JPG, PNG, GIF, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX</div>
</div>
<div class="progress mb-3 d-none" id="uploadProgressContainer">
<div class="progress-bar" role="progressbar" id="uploadProgressBar" style="width: 0%"></div>
</div>
<button type="submit" class="btn btn-primary" id="uploadButton">
<span class="spinner-border spinner-border-sm d-none" id="uploadSpinner"></span>
업로드
</button>
</form>
</div>
</div>
<!-- 파일 검색 섹션 -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">파일 목록</h5>
<div class="d-flex">
<input type="text" class="form-control form-control-sm me-2" id="searchInput" placeholder="파일명 검색...">
<button class="btn btn-sm btn-outline-secondary" id="refreshButton">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>파일명</th>
<th>유형</th>
<th>크기</th>
<th>업로드 일시</th>
<th>작업</th>
</tr>
</thead>
<tbody id="fileListBody">
<!-- 자바스크립트로 데이터 로드 -->
</tbody>
</table>
</div>
<div id="fileListEmpty" class="text-center py-4 d-none">
<p class="text-muted mb-0">파일이 없습니다.</p>
</div>
<div id="fileListLoading" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">로딩 중...</span>
</div>
</div>
<!-- 페이지네이션 -->
<nav id="pagination" class="d-none">
<ul class="pagination justify-content-center" id="paginationContainer">
<!-- 자바스크립트로 페이지네이션 생성 -->
</ul>
</nav>
</div>
</div>
<!-- 파일 상세 모달 -->
<div class="modal fade" id="fileDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="fileDetailTitle">파일 상세</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3 text-center" id="filePreviewContainer">
<!-- 이미지 미리보기가 표시될 곳 -->
</div>
<table class="table table-sm">
<tbody>
<tr>
<th>파일명</th>
<td id="detailFileName"></td>
</tr>
<tr>
<th>키</th>
<td id="detailFileKey"></td>
</tr>
<tr>
<th>유형</th>
<td id="detailFileType"></td>
</tr>
<tr>
<th>크기</th>
<td id="detailFileSize"></td>
</tr>
<tr>
<th>업로드</th>
<td id="detailUploadDate"></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-danger" id="deleteFileBtn">삭제</button>
<button type="button" class="btn btn-primary" id="downloadFileBtn">다운로드</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>
<!-- 파일 업데이트 모달 -->
<div class="modal fade" id="fileUpdateModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">파일 업데이트</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="fileUpdateForm" enctype="multipart/form-data">
<input type="hidden" id="updateFileKey" name="key">
<div class="mb-3">
<label for="updateFileInput" class="form-label">새 파일 선택</label>
<input type="file" class="form-control" id="updateFileInput" name="file" required>
</div>
<div class="progress mb-3 d-none" id="updateProgressContainer">
<div class="progress-bar" role="progressbar" id="updateProgressBar" style="width: 0%"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">취소</button>
<button type="button" class="btn btn-primary" id="updateFileBtn">
<span class="spinner-border spinner-border-sm d-none" id="updateSpinner"></span>
업데이트
</button>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="~/js/file-management.js" asp-append-version="true"></script>
}
// 파일 관리 JavaScript
document.addEventListener('DOMContentLoaded', function() {
// 상수 및 설정
const API_URL = '/api/files';
const PAGE_SIZE = 10;
// DOM 요소
const fileUploadForm = document.getElementById('fileUploadForm');
const fileInput = document.getElementById('fileInput');
const directoryInput = document.getElementById('directory');
const uploadButton = document.getElementById('uploadButton');
const uploadSpinner = document.getElementById('uploadSpinner');
const uploadProgressContainer = document.getElementById('uploadProgressContainer');
const uploadProgressBar = document.getElementById('uploadProgressBar');
const fileListBody = document.getElementById('fileListBody');
const fileListEmpty = document.getElementById('fileListEmpty');
const fileListLoading = document.getElementById('fileListLoading');
const searchInput = document.getElementById('searchInput');
const refreshButton = document.getElementById('refreshButton');
const pagination = document.getElementById('pagination');
const paginationContainer = document.getElementById('paginationContainer');
// 파일 상세 모달 요소
const fileDetailModal = new bootstrap.Modal(document.getElementById('fileDetailModal'));
const fileDetailTitle = document.getElementById('fileDetailTitle');
const filePreviewContainer = document.getElementById('filePreviewContainer');
const detailFileName = document.getElementById('detailFileName');
const detailFileKey = document.getElementById('detailFileKey');
const detailFileType = document.getElementById('detailFileType');
const detailFileSize = document.getElementById('detailFileSize');
const detailUploadDate = document.getElementById('detailUploadDate');
const downloadFileBtn = document.getElementById('downloadFileBtn');
const deleteFileBtn = document.getElementById('deleteFileBtn');
// 파일 업데이트 모달 요소
const fileUpdateModal = new bootstrap.Modal(document.getElementById('fileUpdateModal'));
const fileUpdateForm = document.getElementById('fileUpdateForm');
const updateFileKey = document.getElementById('updateFileKey');
const updateFileInput = document.getElementById('updateFileInput');
const updateFileBtn = document.getElementById('updateFileBtn');
const updateSpinner = document.getElementById('updateSpinner');
const updateProgressContainer = document.getElementById('updateProgressContainer');
const updateProgressBar = document.getElementById('updateProgressBar');
// 상태 변수
let currentPage = 1;
let totalPages = 1;
let fileList = [];
let currentFileKey = null;
let searchTerm = '';
// 초기화
loadFileList();
// 이벤트 리스너
fileUploadForm.addEventListener('submit', handleFileUpload);
searchInput.addEventListener('input', handleSearch);
refreshButton.addEventListener('click', () => loadFileList());
downloadFileBtn.addEventListener('click', downloadCurrentFile);
deleteFileBtn.addEventListener('click', deleteCurrentFile);
updateFileBtn.addEventListener('click', updateCurrentFile);
// 파일 업로드 처리
function handleFileUpload(e) {
e.preventDefault();
const file = fileInput.files[0];
if (!file) {
showToast('warning', '파일을 선택해주세요.');
return;
}
// 파일 크기 검증 (20MB)
if (file.size > 20 * 1024 * 1024) {
showToast('error', '파일 크기는 20MB를 초과할 수 없습니다.');
return;
}
// 파일 형식 검증
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!validExtensions.includes(fileExtension)) {
showToast('error', '지원되지 않는 파일 형식입니다.');
return;
}
// UI 업데이트
setUploading(true);
// FormData 생성
const formData = new FormData();
formData.append('file', file);
const directory = directoryInput.value.trim();
if (directory) {
formData.append('directory', directory);
}
// AJAX 요청
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_URL}/upload`, true);
// 토큰 추가 (인증된 사용자)
const token = getAuthToken();
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
// 진행률 처리
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
uploadProgressBar.style.width = percentComplete + '%';
uploadProgressBar.textContent = percentComplete + '%';
}
});
// 완료 처리
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const response = JSON.parse(xhr.responseText);
showToast('success', '파일이 성공적으로 업로드되었습니다.');
resetUploadForm();
loadFileList(); // 목록 새로고침
} else {
let errorMsg = '파일 업로드 중 오류가 발생했습니다.';
try {
const error = JSON.parse(xhr.responseText);
errorMsg = error.message || errorMsg;
} catch (e) {}
showToast('error', errorMsg);
}
setUploading(false);
};
// 오류 처리
xhr.onerror = function() {
showToast('error', '네트워크 오류가 발생했습니다.');
setUploading(false);
};
xhr.send(formData);
}
// 파일 목록 로드
function loadFileList() {
setLoading(true);
fetch('/api/files/list?page=' + currentPage + '&pageSize=' + PAGE_SIZE + '&search=' + encodeURIComponent(searchTerm), {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + getAuthToken(),
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('목록을 불러오는데 실패했습니다.');
}
return response.json();
})
.then(data => {
fileList = data.items || [];
totalPages = data.totalPages || 1;
renderFileList();
renderPagination();
})
.catch(error => {
console.error('Error:', error);
showToast('error', error.message);
fileList = [];
renderFileList();
})
.finally(() => {
setLoading(false);
});
}
// 파일 목록 렌더링
function renderFileList() {
fileListBody.innerHTML = '';
if (fileList.length === 0) {
fileListEmpty.classList.remove('d-none');
return;
}
fileListEmpty.classList.add('d-none');
fileList.forEach(file => {
const row = document.createElement('tr');
row.innerHTML = `
<td>
<a href="#" class="file-name-link" data-key="${file.key}">${file.fileName}</a>
</td>
<td>${getFileTypeLabel(file.contentType)}</td>
<td>${formatFileSize(file.size)}</td>
<td>${formatDate(file.uploadDate)}</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary preview-btn"
data-key="${file.key}" title="미리보기">
<i class="bi bi-eye"></i>
</button>
<button type="button" class="btn btn-outline-success download-btn"
data-key="${file.key}" title="다운로드">
<i class="bi bi-download"></i>
</button>
<button type="button" class="btn btn-outline-info update-btn"
data-key="${file.key}" title="업데이트">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-outline-danger delete-btn"
data-key="${file.key}" title="삭제">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
fileListBody.appendChild(row);
});
// 이벤트 바인딩
document.querySelectorAll('.file-name-link, .preview-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const key = e.currentTarget.getAttribute('data-key');
showFileDetail(key);
});
});
document.querySelectorAll('.download-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const key = e.currentTarget.getAttribute('data-key');
downloadFile(key);
});
});
document.querySelectorAll('.update-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const key = e.currentTarget.getAttribute('data-key');
showUpdateModal(key);
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const key = e.currentTarget.getAttribute('data-key');
confirmDeleteFile(key);
});
});
}
// 페이지네이션 렌더링
function renderPagination() {
paginationContainer.innerHTML = '';
if (totalPages <= 1) {
pagination.classList.add('d-none');
return;
}
pagination.classList.remove('d-none');
// 이전 버튼
const prevLi = document.createElement('li');
prevLi.className = 'page-item' + (currentPage === 1 ? ' disabled' : '');
prevLi.innerHTML = `<a class="page-link" href="#" data-page="${currentPage - 1}">«</a>`;
paginationContainer.appendChild(prevLi);
// 페이지 번호
const maxButtons = 5;
const startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
const endPage = Math.min(totalPages, startPage + maxButtons - 1);
for (let i = startPage; i <= endPage; i++) {
const pageLi = document.createElement('li');
pageLi.className = 'page-item' + (i === currentPage ? ' active' : '');
pageLi.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
paginationContainer.appendChild(pageLi);
}
// 다음 버튼
const nextLi = document.createElement('li');
nextLi.className = 'page-item' + (currentPage === totalPages ? ' disabled' : '');
nextLi.innerHTML = `<a class="page-link" href="#" data-page="${currentPage + 1}">»</a>`;
paginationContainer.appendChild(nextLi);
// 이벤트 바인딩
document.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetPage = parseInt(e.currentTarget.getAttribute('data-page'));
if (targetPage >= 1 && targetPage <= totalPages && targetPage !== currentPage) {
currentPage = targetPage;
loadFileList();
}
});
});
}
// 파일 상세 정보 표시
function showFileDetail(key) {
const file = fileList.find(f => f.key === key);
if (!file) return;
currentFileKey = key;
fileDetailTitle.textContent = file.fileName;
detailFileName.textContent = file.fileName;
detailFileKey.textContent = file.key;
detailFileType.textContent = getFileTypeLabel(file.contentType);
detailFileSize.textContent = formatFileSize(file.size);
detailUploadDate.textContent = formatDate(file.uploadDate);
// 미리보기 표시
filePreviewContainer.innerHTML = '';
if (isImageFile(file.contentType)) {
getFileUrl(key, true).then(url => {
const img = document.createElement('img');
img.src = url;
img.className = 'img-fluid';
img.style.maxHeight = '200px';
filePreviewContainer.appendChild(img);
});
} else if (isPdfFile(file.contentType)) {
filePreviewContainer.innerHTML = `
<div class="text-center">
<i class="bi bi-file-earmark-pdf text-danger" style="font-size: 3rem;"></i>
<p class="mt-2">PDF 문서</p>
</div>
`;
} else {
filePreviewContainer.innerHTML = `
<div class="text-center">
<i class="bi bi-file-earmark text-primary" style="font-size: 3rem;"></i>
<p class="mt-2">${getFileTypeLabel(file.contentType)}</p>
</div>
`;
}
fileDetailModal.show();
}
// 파일 업데이트 모달 표시
function showUpdateModal(key) {
const file = fileList.find(f => f.key === key);
if (!file) return;
currentFileKey = key;
updateFileKey.value = key;
// UI 초기화
updateProgressBar.style.width = '0%';
updateProgressBar.textContent = '';
updateProgressContainer.classList.add('d-none');
updateFileInput.value = '';
fileUpdateModal.show();
}
// 파일 업데이트 처리
function updateCurrentFile() {
const file = updateFileInput.files[0];
if (!file) {
showToast('warning', '파일을 선택해주세요.');
return;
}
// 파일 크기 검증
if (file.size > 20 * 1024 * 1024) {
showToast('error', '파일 크기는 20MB를 초과할 수 없습니다.');
return;
}
// 업데이트 진행
setUpdating(true);
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('PUT', `${API_URL}/${currentFileKey}`, true);
// 토큰 추가
const token = getAuthToken();
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
// 진행률 처리
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
updateProgressBar.style.width = percentComplete + '%';
updateProgressBar.textContent = percentComplete + '%';
}
});
// 완료 처리
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
showToast('success', '파일이 성공적으로 업데이트되었습니다.');
fileUpdateModal.hide();
loadFileList(); // 목록 새로고침
} else {
let errorMsg = '파일 업데이트 중 오류가 발생했습니다.';
try {
const error = JSON.parse(xhr.responseText);
errorMsg = error.message || errorMsg;
} catch (e) {}
showToast('error', errorMsg);
}
setUpdating(false);
};
// 오류 처리
xhr.onerror = function() {
showToast('error', '네트워크 오류가 발생했습니다.');
setUpdating(false);
};
xhr.send(formData);
}
// 현재 파일 다운로드
function downloadCurrentFile() {
if (currentFileKey) {
downloadFile(currentFileKey);
}
}
// 파일 다운로드
function downloadFile(key) {
// 직접 다운로드 링크로 이동
window.location.href = `${API_URL}/download/${key}`;
}
// 파일 삭제 확인
function confirmDeleteFile(key) {
currentFileKey = key;
if (confirm('정말 이 파일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) {
deleteFile(key);
}
}
// 현재 파일 삭제
function deleteCurrentFile() {
if (currentFileKey) {
if (confirm('정말 이 파일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) {
deleteFile(currentFileKey);
fileDetailModal.hide();
}
}
}
// 파일 삭제
function deleteFile(key) {
fetch(`${API_URL}/${key}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + getAuthToken()
}
})
.then(response => {
if (!response.ok) {
throw new Error('파일 삭제 중 오류가 발생했습니다.');
}
showToast('success', '파일이 성공적으로 삭제되었습니다.');
loadFileList(); // 목록 새로고침
})
.catch(error => {
console.error('Error:', error);
showToast('error', error.message);
});
}
// 파일 URL 가져오기
async function getFileUrl(key, useCdn = true) {
try {
const response = await fetch(`${API_URL}/url/${key}?useCdn=${useCdn}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + getAuthToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('URL을 가져오는데 실패했습니다.');
}
const data = await response.json();
return data.url;
} catch (error) {
console.error('Error:', error);
showToast('error', error.message);
return null;
}
}
// 검색 처리
function handleSearch(e) {
searchTerm = e.target.value.trim();
currentPage = 1; // 첫 페이지로 이동
loadFileList();
}
// UI 상태 함수들
function setLoading(isLoading) {
if (isLoading) {
fileListLoading.classList.remove('d-none');
fileListBody.classList.add('d-none');
pagination.classList.add('d-none');
} else {
fileListLoading.classList.add('d-none');
fileListBody.classList.remove('d-none');
}
}
function setUploading(isUploading) {
if (isUploading) {
uploadButton.disabled = true;
uploadSpinner.classList.remove('d-none');
uploadProgressContainer.classList.remove('d-none');
} else {
uploadButton.disabled = false;
uploadSpinner.classList.add('d-none');
uploadProgressContainer.classList.add('d-none');
}
}
function setUpdating(isUpdating) {
if (isUpdating) {
updateFileBtn.disabled = true;
updateSpinner.classList.remove('d-none');
updateProgressContainer.classList.remove('d-none');
} else {
updateFileBtn.disabled = false;
updateSpinner.classList.add('d-none');
updateProgressContainer.classList.add('d-none');
}
}
function resetUploadForm() {
fileUploadForm.reset();
uploadProgressBar.style.width = '0%';
uploadProgressBar.textContent = '';
}
// 유틸리티 함수들
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDate(dateString) {
const date = new Date(dateString);
return new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute
Docker 컨테이너 안의 ASP.NET Core 8 에서 로컬 PC의 MSSQL 연결하기 (0) | 2025.04.18 |
---|---|
ASP.NET Core 8 환경, NLB(네트워크 로드 밸런싱) 세션(Session) 조치 제안방법 찾기 (0) | 2025.04.02 |
MCP를 활용한 ASP.NET Core 9 애플리케이션 (0) | 2025.04.02 |
ASP.NET Core : 정적 파일 제공 시 보안적인 측면 고려 (0) | 2025.03.27 |
ASP.NET Core + SixLabors 이모티콘 생성 및 실시간 미리보기 기능 (0) | 2025.03.14 |