MVC

ASP.NET Core MVC로 글 작성으로 첨부 파일 올리고 다운로드 가능하게 하기

알럽유 2024. 8. 7. 10:43
728x90
반응형

오늘은 사진과 같이 글 작성을 하면서 파일 업로드 & 다운로드를 할 수 있는 방법에 대해서 알려드리겠습니다.

PostPage

<div class="Main-container">
    <h2 class="h2">새 게시글 작성</h2>
    <div class="container">
        <form asp-action="PostPage" method="post" enctype="multipart/form-data">
            @Html.AntiForgeryToken()
            <div class="form-group">
                <label asp-for="Title" class="label">제목</label>
                <input asp-for="Title" class="input" id="Title" />
                <span asp-validation-for="Title" class="text"></span>
            </div>
            <div class="form-group">
                <label asp-for="Content" class="label">내용</label>
                <textarea asp-for="Content" class="textarea" id="Content"></textarea>
                <span asp-validation-for="Content" class="text"></span>
            </div>
            <div class="form-group">
                <label asp-for="Author" class="label">작성자</label>
                <input asp-for="Author" class="input" id="Author" />
                <span asp-validation-for="Author" class="text"></span>
            </div>
            <div class="form-group">
                <label for="uploadedFile" class="label">파일 업로드 (필수 사항)</label>
                <input type="file" name="uploadedFile" id="uploadedFile" class="input" />
                <span asp-validation-for="Filepath" class="text"></span>
            </div>
            <div class="form-group">
                <input asp-for="IsPopup" type="checkbox" id="IsPopup" />
                <label asp-for="IsPopup">팝업창으로 만들기</label>
            </div>
            <button type="submit" class="btn btn-primary">작성</button>
        </form>
    </div>
</div>

글작성 화면입니다. 파일 업로드를 위해 multipart/form-data 인코딩 타입을 사용합니다.

 

글작성 Controller

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PostPage(Noticeboard post, IFormFile uploadedFile)
{
    if (ModelState.IsValid)
    {
        try
        {
            if (uploadedFile != null && uploadedFile.Length > 0)
            {
                // 파일 확장자 검증 추가
                if (!FileHelper.IsValidFileExtension(uploadedFile.FileName))
                {
                    ViewBag.ErrorMessage = "Invalid file extension.";
                    return View(post);
                }

                var safeFileName = FileHelper.MakeSafeFilename(uploadedFile.FileName);
                var filePath = Path.Combine("wwwroot/uploads", safeFileName);
                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await uploadedFile.CopyToAsync(stream);
                }
                post.Filepath = "/uploads/" + safeFileName;
                post.Filename = safeFileName;
            }
            else
            {
                post.Filepath = string.Empty;
                post.Filename = string.Empty;
            }

            post.Date = DateTime.Now;
            post.Views = 0;
            await _crudService.CreatePostAsync(post);

            if (post.IsPopup)
            {
                var popupUrl = Url.Action("Popup", "Announcement", new { postId = post.Id });
                if (string.IsNullOrEmpty(HttpContext.Session.GetString("PopupUrl1")))
                {
                    HttpContext.Session.SetString("PopupUrl1", popupUrl);
                    Response.Cookies.Append("PopupUrl1", popupUrl, new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(1) });
                }
                else if (string.IsNullOrEmpty(HttpContext.Session.GetString("PopupUrl2")))
                {
                    HttpContext.Session.SetString("PopupUrl2", popupUrl);
                    Response.Cookies.Append("PopupUrl2", popupUrl, new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(1) });
                }
                else
                {
                    HttpContext.Session.SetString("PopupUrl3", popupUrl);
                    Response.Cookies.Append("PopupUrl3", popupUrl, new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(1) });
                }
            }

            return RedirectToAction("ManagerList");
        }
        catch (Exception ex)
        {
            ViewBag.ErrorMessage = "Error creating post: " + ex.Message;
        }
    }
    else
    {
        ViewBag.ErrorMessage = "ModelState is not valid: " + string.Join("; ", ModelState.Values
                .SelectMany(x => x.Errors)
                .Select(x => x.ErrorMessage));
    }
    return View(post);
}

컨트롤러에서는 폼에서 전송된 데이터를 받아서 파일을 서버에 저장하고, 데이터베이스에 파일 경로 정보를 저장합니다.

 

파일 확장자 검증

파일 확장자를 검증하여 특정 파일 형식만 허용하도록 할 수 있습니다.

 

 

 

 

FileHelper.cs

using System;
using System.IO;
using System.Linq;

namespace noticeboard.Helpers
{
    public static class FileHelper
    {
        public static string MakeSafeFilename(string filename)
        {
            var invalidChars = Path.GetInvalidFileNameChars();
            return string.Concat(filename.Select(ch => invalidChars.Contains(ch) ? '_' : ch)); // 유효하지 않은 문자는 언더스코어('_')로 대체
        }

        public static bool IsValidFileExtension(string filename)
        {
            var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".pdf" };
            var extension = Path.GetExtension(filename).ToLowerInvariant();
            return allowedExtensions.Contains(extension);
        }
    }
}

 

이 메서드는 파일 이름을 입력받아 안전한 파일 이름으로 변환하여 반환합니다.

 

  1. Path.GetInvalidFileNameChars():
    • 이 메서드는 파일 이름에 사용할 수 없는 문자의 배열을 반환합니다. 예를 들어, Windows에서는 \, /, :, *, ?, ", <, >, |와 같은 문자들이 유효하지 않습니다.
  2. filename.Select(ch => invalidChars.Contains(ch) ? '_' : ch):
    • filename 문자열의 각 문자를 순회하며, 그 문자가 유효하지 않은 문자인지 검사합니다.
    • 만약 유효하지 않은 문자라면, 해당 문자를 언더스코어('_')로 대체합니다.
    • 유효한 문자라면 그대로 유지합니다.
  3. string.Concat(...):
    • 변환된 문자들을 연결하여 새로운 문자열을 만듭니다.
  4. 파일 확장자 검증 - 파일 확장자를 검증하여 특정 파일 형식만 허용하도록 할 수 있습니다.

 

이 FileHelper는 constroller에 잘 사용되고 있습니다.

 

그리고 마지막 리스트 페이지입니다.

@model X.PagedList.IPagedList<noticeboard.Models.Noticeboard>
<link rel="stylesheet" href="~/css/Announcement/List.css" asp-append-version="true" />
@using X.PagedList.Mvc.Core

@{
    ViewData["Title"] = "ListPage";
}

<div class="Main-container">
    <h2 class="h2">게시글 목록</h2>

    @if (Model == null || !Model.Any())
    {
        <p>게시글이 없습니다.</p>
    }
    else
    {
        <table class="table">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>첨부파일</th>
                    <th>등록일</th>
                    <th>등록자</th>
                    <th>조회수</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var post in Model)
                {
                    <tr>
                        <td>@post.Id</td>
                        <td>@Html.ActionLink(post.Title, "Details", new { id = post.Id })</td>
                        <td>
                            @if (!string.IsNullOrEmpty(post.Filepath))
                            {
                                <a href="@Url.Action("DownloadFile", "Announcement", new { filePath = post.Filepath, fileName = post.Filename })">첨부파일</a>
                            }
                        </td>
                        <td>@post.Date.ToShortDateString()</td>
                        <td>@post.Author</td>
                        <td>@post.Views</td>
                    </tr>
                }
            </tbody>
        </table>
        <div>
            @Html.PagedListPager(Model, page => Url.Action("ListPage", new { page }), new PagedListRenderOptions
   {
       UlElementClasses = new[] { "pagination" },
       LiElementClasses = new[] { "page-item" },
       PageClasses = new[] { "page-link" }
   })
        </div>
    }
</div>