티스토리 뷰

반응형

Spring File Upload

Spring Boot + Thymeleaf

코드와 주석 위주로 작성하였습니다.

파일 업로드 테스트에 초점을 맞춰 작성해본 코드로 추가 설명이 필요하시거나 에러 발생 시 댓글 남겨주시면 대단히 감사하겠습니다..! 😊🎉


Add Dependency

<!-- ... -->
    <!-- File Upload (대용량 파일 처리) -->
    <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
    <!-- File Upload (file upload 기능 사용) -->
    <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
<!-- ... -->

pom.xml (all view)

<!-- All dependency -->
    <!-- h2database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- File Upload (대용량 파일 처리) -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
    <!-- File Upload (file upload 기능 사용) -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>

    <!-- Jackson Library -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
    </dependency>

    <!-- junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>
<!-- ... -->

Set Properties

# total file size cannot exceed 10MB.
spring.servlet.multipart.max-file-size=10MB
# total request size for a multipart/form-data cannot exceed 20MB.
spring.servlet.multipart.max-request-size=20MB

application.properties (all view)

### PORT setting
server.port = 8999

### datasource Setting
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=sa
spring.h2.console.enabled=true

### JPA Query Setting
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.type.descriptor.sql=trace

### File Upload Setting
# total file size cannot exceed 10MB.
spring.servlet.multipart.max-file-size=10MB
# total request size for a multipart/form-data cannot exceed 20MB.
spring.servlet.multipart.max-request-size=20MB

# Logger Setting
logging.level.root=INFO

Config

Spring Audit 기능을 활용하여 도메인들이 공통적으로 가지고 있는 필드 처리

JpaConfig.java

@Configuration
@EnableJpaAuditing // JPA Auditing 활성화
public class JpaConfig {
}

Domain

BaseTimeEntity.java

/**
 * 모든 Entity의 상위 클래스
 * Entity들의 생성시간, 수정시간을 자동으로 관리
 *
 * @MappedSuperclass : 상속 클래스들이 부모 클래스의 컬럼을 인식하도록 설정
 * @EntityListeners(AuditingEntityListener.class) : Auditing 기능
 */
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDateTime;

    @LastModifiedDate
    private LocalDateTime modifiedDateTime;
}

GalleryDto.java

public class GalleryDto {

    @Getter
    @NoArgsConstructor
    public static class Response {
        private Long id;
        private String memberName;
        private String title;
        private String contents;
        private LocalDateTime createdDateTime;


        public Response(Gallery entity) {
            this.id = entity.getId();
            this.memberName = entity.getMemberName();
            this.title = entity.getTitle();
            this.contents = entity.getContents();
            this.createdDateTime = entity.getCreatedDateTime();
        }
    }

    @Getter
    public static class ListResponse {
        private Long id;
        private String member;
        private String title;

        public ListResponse(Gallery entity) {
            this.id = entity.getId();
            this.member = entity.getMemberName();
            this.title = entity.getTitle();
        }
    }
}

Entity

Gallery.java

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity(name="Gallery")
@Table(name="GALLERY")
public class Gallery extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="GALLERY_ID")
    private Long id;

    @Column(name = "MEMBER_NAME", nullable = false, updatable = false)
    private String memberName;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String contents;

    @OneToMany(mappedBy="gallery", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
    private List<Attachments> attachmentsList = new ArrayList<>();
}

Member.java

@Getter
@NoArgsConstructor
@Entity(name="Member")
@Table(name = "MEMBER")
public class Member extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID", unique = true, nullable = false)
    private Long id;

    @Column(length = 15, nullable = false)
    private String name;

    @Column(length = 100, nullable = false)
    private String password;

    @Column(length = 50, nullable = false)
    private String email;

    @Column(length = 20, nullable = false)
    private String contact;

    @Builder
    public Member(String name, String password, String email, String contact) {
        this.name = name;
        this.password = password;
        this.email = email;
        this.contact = contact;
    }
}

Attachments.java

@Getter
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity(name="Attachments")
@Table(name="ATTACHMENTS")
public class Attachments extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ATTACHMENTS_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "GALLERY_ID")
    private Gallery gallery;

    @Column(nullable = false)
    private String origFileName;

    @Column(nullable = false)
    private String fileName;

    @Column(nullable = false)
    private String filePath;

    private Long fileSize;

    @Builder
    public Attachments(Gallery gallery, String origFileName, String fileName, String filePath, Long fileSize) {
        this.gallery = gallery;
        this.origFileName = origFileName;
        this.fileName = fileName;
        this.filePath = filePath;
        this.fileSize = fileSize;
    }

    public void setGallery(Gallery gallery) {
        this.gallery = gallery;
    }
}

Exception

FileException.java

// serialVersionUID를 정의해주지 않은 경우 나타나는 warnning을 체크하지 않음
@SuppressWarnings("serial")
public class FileException extends RuntimeException {

    public FileException(String message) {
        super(message);
    }

    public FileException(String message, Throwable cause) {
        super(message, cause);
    }
}

Repository

AttachmentsRepository.java

public interface AttachmentsRepository extends JpaRepository<Attachments, Long> {

    /**
     * Find attachments by the gallery ID
     *
     * @param galleryId
     * @return
     */
    public List<Attachments> findAllByGalleryId(Long galleryId);

    /**
     * Delete attachments by the delete file list
     *
     * @param deleteFileList
     */
    @Modifying
    @Query(value = "DELETE FROM Attachments a " +
                  "WHERE a.id IN (:deleteFileList)")
    public void deleteByAttachIdList(@Param("deleteFileList") List<Long> deleteFileList);
}

GalleryRepository.java

public interface GalleryRepository extends JpaRepository<Gallery, Long> {

    List<Gallery> findAllByOrderByIdDesc();
}

MemberRepository.java

public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("SELECT m "
            + "FROM Member m "
            + "WHERE m.name = :name")
    Optional<Member> findByName(@Param("name") String name);
}

Service

GalleryServiceImpl.java

@RequiredArgsConstructor
@Service
@Getter
public class GalleryServiceImpl {

    private final GalleryRepository galleryRepository;
    private final AttachmentsRepository attachmentsRepository;

    @Transactional(readOnly = true)
    public GalleryDto.Response searchById(Long id) {

        Gallery entity = galleryRepository.findById(id).orElseThrow(()
                -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다."));

        return new GalleryDto.Response(entity);
    }

    @Transactional(readOnly = true)
    public List<GalleryDto.ListResponse> searchAllDesc() {

        return galleryRepository.findAllByOrderByIdDesc().stream()
                .map(GalleryDto.ListResponse::new)
                .collect(Collectors.toList());
    }

    @Transactional
    public void delete(Long id){

        Gallery gallery = galleryRepository.findById(id)
                .orElseThrow(()->new IllegalArgumentException("해당 게시물이 존재하지 않습니다."));

        galleryRepository.delete(gallery);
    }


    @Transactional
    public Long save(Gallery gallery, List<MultipartFile> files, List<Long> deleteFileList) throws Exception {

        Gallery saveGallery = getGalleryRepository().save(gallery);

        List<Attachments> AttachmentsList = FileUtilities.parseFileInfo(files, saveGallery);

        // 파일이 존재할 경우
        if (!AttachmentsList.isEmpty()) {
            AttachmentsList.forEach(attachments -> attachmentsRepository.save(attachments));
        }

        // 삭제할 파일이 존재할 경우
        if (!deleteFileList.isEmpty()) {
            attachmentsRepository.deleteByAttachIdList(deleteFileList);
        }

        return saveGallery.getId();
    }
}

MemberServiceImpl.java

@RequiredArgsConstructor
@Service
@Getter
public class MemberServiceImpl {

    private final MemberRepository memberRepository;

    @Transactional(readOnly = true)
    public Member searchByName(String name) {

        Member member = memberRepository.findByName(name).orElseThrow(()
                -> new IllegalArgumentException("존재하지 않는 회원입니다."));

        return member;
    }
}

Utility

FileUtilities.java

public class FileUtilities {

    // Paths.get()으로 운영체제에 따라서 다른 파일구분자 처리
    public final static String rootPath = Paths.get("C:", "Users", "jihun.park", "Desktop", "testFile").toString();

    /**
     * MultipartFile 형태의 파일을 Attachments Entity 형태로 파싱
     *
     * @param multipartFiles
     * @param gallery
     */
    public static List<Attachments> parseFileInfo(List<MultipartFile> multipartFiles, Gallery gallery) throws Exception {

        // 파일이 첨부되지 않았을 경우
        if (CollectionUtils.isEmpty(multipartFiles)) {
            return Collections.emptyList();
        }

        // 파일 업로드 경로 생성
        String savePath = Paths.get(rootPath, "files").toString();
        if (!new File(savePath).exists()) {
            try {
                new File(savePath).mkdir();
            } catch (Exception e) {
                e.getStackTrace();
            }
        }

        List<Attachments> fileList = new ArrayList<>();

        for (MultipartFile multipartFile : multipartFiles) {

            String origFilename = multipartFile.getOriginalFilename();
            if (origFilename == null || "".equals(origFilename)) continue;
            String filename = MD5Generator(FilenameUtils.getBaseName(origFilename)).toString() + "." + FilenameUtils.getExtension(origFilename);
            String filePath = Paths.get(savePath, filename).toString();

            Attachments attachments = new Attachments(gallery,
                                                    multipartFile.getOriginalFilename(),
                                                    filename,
                                                    filePath,
                                                    multipartFile.getSize());

            fileList.add(attachments);

            try {
                File file = new File(filePath);
                multipartFile.transferTo(file);

                // 파일 권한 설정(쓰기, 읽기)
                file.setWritable(true);
                file.setReadable(true);

            } catch (IOException e) {
                throw new FileException("[" + multipartFile.getOriginalFilename() + "] failed to save file...");

            } catch (Exception e) {
                throw new FileException("[" + multipartFile.getOriginalFilename() + "] failed to save file...");
            }
        }

        return fileList;
    }

    /**
     * 다운로드 받을 파일 생성
     *
     * @param attach
     */
    public static File getDownloadFile(Attachments attach) {

        return new File(Paths.get(rootPath, "files").toString(), attach.getFileName());
    }

    /**
     * 파일명 중복 방지를 위해 MD5(128비트 암호화 해시 함수) 파일명 생성
     *
     * @param input
     */
    public static String MD5Generator(String input) throws UnsupportedEncodingException, NoSuchAlgorithmException {

        MessageDigest mdMD5 = MessageDigest.getInstance("MD5");
        mdMD5.update(input.getBytes("UTF-8"));

        byte[] md5Hash = mdMD5.digest();
        StringBuilder hexMD5hash = new StringBuilder();

        for(byte b : md5Hash) {
            String hexString = String.format("%02x", b);
            hexMD5hash.append(hexString);
        }

        return hexMD5hash.toString();
    }

    /**
     * MediaType 생성
     *
     * @param filename
     */
    public static MediaType getMediaType(String filename) {

        String contentType = FilenameUtils.getExtension(filename);
        MediaType mediaType = null;

        if (contentType.equals("png")) {
            mediaType = MediaType.IMAGE_PNG;
        } else if (contentType.equals("jpeg") || contentType.equals("jpg")) {
            mediaType = MediaType.IMAGE_JPEG;
        } else if (contentType.equals("gif")) {
            mediaType = MediaType.IMAGE_GIF;
        }

        return mediaType;
    }
}

Controller

  • Controller에서 가장 중요한 부분은 다중 파일을 클라이언트로부터 전달받기 위해 MultipartHttpServletRequest 를 매개변수로 두어야 한다.
  • MultipartHttpServletRequest에는 files와 parameter가 같이 담겨져 온다.

GalleryApiController.java

@RequiredArgsConstructor
@RestController
public class GalleryApiController {

    private static final Logger LOGGER = LoggerFactory.getLogger(GalleryApiController.class);

    private final GalleryServiceImpl galleryService;
    private final MemberServiceImpl memberService;

    /**
     * 게시물 삭제
     *
     * @param galleryId
     */
    @PostMapping("/gallery/delete")
    public void deleteGallery(@RequestParam("galleryId") Long galleryId){

        // 관리자 or 작성자가 아닐 경우 삭제 불가 로직

        galleryService.delete(galleryId);
    }

    /**
     * 게시물 저장
     *
     * @param multiRequest
     * @return
     * @throws Exception
     */
    @PostMapping("/gallery")
    @ResponseStatus(HttpStatus.CREATED)
    public String saveGallery(MultipartHttpServletRequest multiRequest) throws Exception {

        LOGGER.debug(multiRequest.getParameter("id"));
        JsonObject jsonObject = new JsonObject();

        // 작성자 정보 확인
        Optional<Member> optMember = memberService.getMemberRepository().findByName(multiRequest.getParameter("memberName"));
        if (!optMember.isPresent()) {
            jsonObject.addProperty("response", "error");
            jsonObject.addProperty("errorMsg", "사용자 정보를 찾을 수 없습니다.");

            return jsonObject.toString();
        }

        Gallery gallery = new Gallery();
        List<Long> deleteFileList = new ArrayList<>();

        // 신규 등록
        if (multiRequest.getParameter("id") == null) {
            gallery.setMemberName(multiRequest.getParameter("memberName"));
            gallery.setTitle(multiRequest.getParameter("title"));
            gallery.setContents(multiRequest.getParameter("contents"));
        }
        // 수정
        else {
            Optional<Gallery> optGallery = galleryService.getGalleryRepository().findById(Long.parseLong(multiRequest.getParameter("id")));
            if (!optGallery.isPresent()) {
                jsonObject.addProperty("response", "error");
                jsonObject.addProperty("errorMsg", "게시물 정보를 찾을 수 없습니다.");

                return jsonObject.toString();
            }

            gallery = optGallery.get();
            gallery.setTitle(multiRequest.getParameter("title"));
            gallery.setContents(multiRequest.getParameter("contents"));

            if (!"".equals(multiRequest.getParameter("deleteFiles"))) {
                deleteFileList = Arrays.asList(multiRequest.getParameter("deleteFiles").split(",")).stream()
                                            .map(s -> Long.parseLong((String) s)).collect(Collectors.toList());
            }
        }

        Long id = galleryService.save(gallery, multiRequest.getFiles("files"), deleteFileList);

        jsonObject.addProperty("response", "OK");
        jsonObject.addProperty("galleryId", id);

        return jsonObject.toString();
    }

    /**
     * 첨부 파일 다운로드
     *
     * @param id
     * @param response
     */
    @GetMapping("/gallery/download/{id}")
    public void downloadAttach(@PathVariable Long id, HttpServletResponse response){

        Optional<Attachments> optAttach = galleryService.getAttachmentsRepository().findById(id);
        if (!optAttach.isPresent()) {
            throw new RuntimeException("파일을 찾을 수 없습니다.");
        }

        Attachments attach = optAttach.get();
        File file = FileUtilities.getDownloadFile(attach);

        try {
            byte[] data = FileUtils.readFileToByteArray(file);

            response.setContentType(FileUtilities.getMediaType(attach.getOrigFileName()).toString());
            response.setContentLength(data.length);
            response.setHeader("Content-Transfer-Encoding", "binary");
            response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(attach.getOrigFileName(), "UTF-8") + "\";");

            response.getOutputStream().write(data);
            response.getOutputStream().flush();
            response.getOutputStream().close();

        } catch (IOException e) {
            throw new RuntimeException("파일 다운로드에 실패하였습니다.");
        } catch (Exception e) {
            throw new RuntimeException("시스템에 문제가 발생하였습니다.");
        }
    }
}

GalleryController.java

@Controller
@RequestMapping("/gallery")
public class GalleryController {

    @Autowired private GalleryServiceImpl galleryService;

    /**
     * View Gallery
     *
     * @param id
     * @param model
     * @return
     */
    @GetMapping("/{id}")
    public String searchById(@PathVariable Long id, Model model) {

        model.addAttribute("gallery", galleryService.searchById(id));
        model.addAttribute("fileList", galleryService.getAttachmentsRepository().findAllByGalleryId(id));

        return "gallery/viewGallery";
    }

    /**
     * Move Gallery Edit Page
     *
     * @param model
     * @return
     */
    @GetMapping("/edit")
    public String newGallery(Model model) {

        model.addAttribute("gallery", new GalleryDto.Response());

        return "gallery/editGallery";
    }

    /**
     * Edit Gallery
     *
     * @param id
     * @param model
     * @return
     */
    @GetMapping("/edit/{id}")
    public String editGallery(@PathVariable Long id, Model model) {

        // 관리자 or 작성자가 아닐 경우 수정 불가 로직

        model.addAttribute("gallery", galleryService.searchById(id));
        model.addAttribute("fileList", galleryService.getAttachmentsRepository().findAllByGalleryId(id));

        return "gallery/editGallery";
    }

    /**
     * Display Img
     *
     * @param id
     * @return
     * @throws Exception
     */
    @GetMapping(value = "/display")
    public ResponseEntity<byte[]> displayImgFile(@RequestParam("id") Long id)throws Exception{

        InputStream in = null;
        ResponseEntity<byte[]> entity = null;
        Optional<Attachments> optAttach = galleryService.getAttachmentsRepository().findById(id);
        if(!optAttach.isPresent()) {
            new RuntimeException("이미지 정보를 찾을 수 없습니다.");
        }

        Attachments attach = optAttach.get();

        try {
            HttpHeaders headers = new HttpHeaders();
            in = new FileInputStream(attach.getFilePath());
            headers.setContentType(FileUtilities.getMediaType(attach.getOrigFileName()));
            headers.add("Content-Disposition", "attachment; filename=\"" + new String(attach.getOrigFileName().getBytes("UTF-8"), "ISO-8859-1")+"\"");

            entity = new ResponseEntity<byte[]>(IOUtils.toByteArray(in), headers, HttpStatus.CREATED);

        } catch(Exception e) {
            e.printStackTrace();
            entity = new ResponseEntity<byte[]>(HttpStatus.BAD_REQUEST);
        } finally {
            in.close();
        }

        return entity;
    }
}

View

  • 클라이언트 단에서 중요한 부분은 multipart/form-data type으로 서버에 데이터를 전송해야 한다.

thymeleaf

  • Edit
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>File Upload Test</title>
    <link rel='stylesheet' href='/css/bootstrap.min.css'>
</head>
<style>
.btn-xs {
  padding: .25rem .4rem;
  font-size: .700rem;
  line-height: .7;
  border-radius: .2rem;
}
</style>
<body>
<div class="container">
    <form id="form" enctype="multipart/form-data" th:object="${gallery}" onsubmit="return false">
        <input type="hidden" id="id" th:if="*{id != null and id > 0}" th:field="*{id}" />
        <div class="form-group row">
            <label for="title" class="col-sm-2 col-form-label"><strong>제목</strong></label>
            <div class="col-sm-10">
                <input type="text" name="title" class="form-control" id="title" th:value="*{title}"></input>
            </div>
        </div>
        <div class="form-group row">
            <label for="memberId" class="col-sm-2 col-form-label"><strong>작성자</strong></label>
            <div class="col-sm-10">
                <input type="text" name="memberName" class="form-control" id="memberName" th:value="*{memberName}">
            </div>
        </div>
        <div class="form-group row">
            <label for="contents" class="col-sm-2 col-form-label"><strong>내용</strong></label>
            <div class="col-sm-10">
                <textarea name="contents" class="form-control" id="contents">[[*{contents}]]</textarea>
            </div>
        </div>
        <div class="form-group row">
            <label for="files" class="col-sm-2 col-form-label"><strong>첨부 파일</strong></label>
            <div class="col-sm-10">
                <div class="custom-file" id="inputFile">
                    <input name="files" id="files" type="file" multiple="multiple" class="custom-file-input">
                    <label class="custom-file-label" for="customFile">파일을 선택해 주세요.</label>
                </div>
            </div>
            <div id="fileList" style="margin:40px;">
                <strong>첨부파일 : </strong>
                <br>
                <div id=uploadFileList>
                </div>
            </div>
            <br>
            <div th:if="${not #lists.isEmpty( fileList )}">
                <div id="fileList" style="margin:40px;">
                    <strong>기존 첨부파일 : </strong>
                    <div th:each="row, status : ${fileList}" >
                        <img th:src = "@{'/gallery/display?id=' + ${row.id}}" width="80"/>
                        <a th:href="@{'/gallery/download/' + ${row.id}}" th:text="${row.origFileName}"></a>
                        &nbsp;<button type="button" onclick="deleteFile(this)" th:value="${row.id}" class="btn btn-danger btn-xs">X</button>
                    </div>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-auto mr-auto"></div>
            <div class="col-auto">
                <input class="btn btn-primary" type="submit" id="btnSave" role="button" value="글쓰기">
            </div>
        </div>
    </form>
</div>
<script src="/js/jquery-3.6.0.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script type="text/javascript" th:inline="javascript">
/*<![CDATA[*/
$(function() {
    $("#files").on("change", function(e) {
        inputFile(e);
    });

    $("#btnSave").on("click", function() {
        saveGallery();
    });
});

var fileCount = 0;
var totalCount = 10;
var fileNum = 0;
var inputFileList = new Array();
var deleteFileList = new Array();

function inputFile(e) {

    var files = e.target.files;

    var filesArr = Array.prototype.slice.call(files);

    if (fileCount + filesArr.length > totalCount) {
        alert('파일은 최대 ' + totalCount + '개까지 업로드 할 수 있습니다.');
        return;
    } else {
        fileCount = fileCount + filesArr.length;
    }

    filesArr.forEach(function(f) {
        var reader = new FileReader();
        reader.onload = function(e) {
            inputFileList.push(f);
            $('#uploadFileList').append('<div id="file' + fileNum + '" onclick="fileDelete(\'file' + fileNum + '\')">' + '<font style="font-size:15px">' + f.name + '</font><div/>');
            fileNum++;
        };
        reader.readAsDataURL(f);
    });
    $("#files").val("");
}

function deleteFile(elem) {

    console.log(elem.value);
    $(elem).parent().remove();

    if (!deleteFileList.includes(elem.value)) {
        deleteFileList.push(elem.value);
    }
}

function fileDelete(fileNum) {

    var no = fileNum.replace(/[^0-9]/g, "");
    inputFileList[no].is_delete = true;
    $('#' + fileNum).remove();
    fileCount--;
}


function saveGallery() {

     var formData = new FormData($("form")[0]);

    formData.append("deleteFiles", deleteFileList);

    for (var i = 0; i < inputFileList.length; i++) {
        if(!inputFileList[i].is_delete){
             formData.append("files", inputFileList[i]);
        }
    }

    $.ajax({
        type : "POST",
        enctype : "multipart/form-data",
        url : "/gallery",
        data : formData,
        dataType:"json",
        processData : false,
        contentType : false,
        success : function(result) {
            console.log(result);
            if (result.response == "OK") {
                if ($("#id").val() == undefined) {
                    alert("저장되었습니다.");
                } else {
                    alert("수정되었습니다.");
                }

                location.href = "/gallery/edit/" + result.galleryId;
            } else {
                alert(result.errorMsg);
            }
        },
    });
}
/*]]>*/
</script>
</body>
</html>
  • View
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title th:text="@{'게시판 - ' + ${gallery.title}}"></title>
    <link rel="stylesheet" href="/css/bootstrap.min.css" />
  </head>
  <body>
    <div class="container">
      <div class="card">
        <div class="card-body">
          <h5
            class="card-title"
            th:text="@{${gallery.title} + ' - ' + ${gallery.memberName}}"
          ></h5>
          <p class="card-text">
            <small
              class="text-muted"
              th:text="${#temporals.format(gallery.createdDateTime, 'yyyy-MM-dd HH:mm')}"
            ></small>
          </p>
          <p class="card-text" th:text="${gallery.contents}"></p>
          <div th:if="${not #lists.isEmpty( fileList )}">
            <strong>첨부파일 : </strong>
            <div th:each="row, status : ${fileList}" class="card-text">
              <img th:src="@{'/gallery/display?id=' + ${row.id}}" width="80" />
              <a
                th:href="@{'/gallery/download/' + ${row.id}}"
                th:text="${row.origFileName}"
              ></a>
            </div>
          </div>
        </div>
      </div>
      <div class="row mt-3">
        <div class="col-auto mr-auto"></div>
        <div class="col-auto">
          <a
            class="btn btn-info"
            th:href="@{'/gallery/edit/' + ${gallery.id}}"
            role="butEdit"
            >수정</a
          >
        </div>
        <div class="col-auto">
          <form id="delete-form" th:action="@{'/gallery/delete'}" method="post">
            <input type="hidden" name="galleryId" th:value="${gallery.id}" />
            <button id="delete-btn" type="submit" class="btn btn-danger">
              삭제
            </button>
          </form>
        </div>
      </div>
    </div>
    <script src="/js/jquery-3.6.0.min.js"></script>
    <script src="/js/bootstrap.min.js"></script>
    <script type="text/javascript" th:inline="javascript">
      /*<![CDATA[*/
      /*]]>*/
    </script>
  </body>
</html>

Result

Result

Reference

https://congsong.tistory.com/39?category=749196

https://tyrannocoding.tistory.com/54

https://congsong.tistory.com/45?category=749196

Guide

Uploading Files

Project Code

Github

반응형
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday