이전글 보기

 

https://aamoos.tistory.com/688

 

[Spring Jpa] 16. 게시판 만들기 - 첨부파일 다운로드

목표 - 저번장에서 파일업로드후 테이블에 데이터를 쌓고, 파일을 저장하는것까지 하였습니다. 이번장에서는 업로드한 파일을 상세에서 보여주고 다운로드를 할수있는것을 개발해보겠습니다.

aamoos.tistory.com

 

 

목표

- 이번장에서는 첨부파일 삭제버튼을 클릭하면 첨부파일 DEL_YN을 Y로 업데이트 하는부분을 개발해보겠습니다.

 

update.html

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/default_layout}">

<head layout:fragment="css">
    <style>
         .fieldError {
             border-color: #bd2130;
         }

         .form-group p{
            color: red;
         }

    </style>
</head>

<div layout:fragment="content" class="content">
    <form th:action="@{/update/ + *{id}}" th:object="${boardDto}" method="post">
        <input type="hidden" name="_method" value="put"/>
        <input type="hidden" id="boardId" name="id" th:value="*{id}"/>
        <article>
            <div class="container" role="main">
                <div class="form-group">
                    <label for="title">제목</label>
                    <input type="text" class="form-control" id="title" name="title" th:value="*{title}"
                           placeholder="제목을 입력해 주세요"
                           th:class="${#fields.hasErrors('title')}? 'form-control fieldError' : 'form-control'">
                    <p th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Incorrect date</p>
                </div>
                <br>
                <div class="mb-3">
                    <label for="reg_id">작성자</label>
                    <input type="text" class="form-control" id="reg_id" name="regId" value="관리자" readonly>
                </div>
                <br>
                <div class="mb-3">
                    <label for="content">내용</label>
                    <textarea class="form-control" rows="5" id="content" name="content" th:text="*{content}"
                              placeholder="내용을 입력해 주세요"></textarea>
                </div>
                <br>
                <div class="mb-3">
                    <label for="content">첨부파일</label>
                    <p th:each="boardFile, index : ${boardFile}">
                        <a th:href="@{/fileDownload/{boardId}(boardId=${boardFile.fileId})}"
                           th:text="${boardFile.originFileName}">파일이름1.png</a>
                        <span>
                <button th:fileId="${boardFile.boardFileId}" th:onclick="boardDelete(this.getAttribute('fileId'))"
                        type="button" class="btn btn-outline-danger">삭제</button>
            </span>
                    </p>
                </div>
                <br>
                <div>
                    <button type="submit" class="btn btn-sm btn-primary" id="btnSave">수정</button>
                    <button onclick="location.href='/'" type="button" class="btn btn-sm btn-primary" id="btnList">목록
                    </button>
                </div>
            </div>
        </article>
    </form>
</div>
</html>

<script>
  //글삭제
    function boardDelete(fileId){
        if (confirm("정말로 삭제하시겠습니까?")) {
            //배열생성
            const form = document.createElement('form');
            form.setAttribute('method', 'post');        //Post 메소드 적용
            form.setAttribute('action', '/boardFileDelete');

            //파일 id
            var input1 = document.createElement('input');
            input1.setAttribute("type", "hidden");
            input1.setAttribute("name", "fileId");
            input1.setAttribute("value", fileId);

            //게시판 id
            const selectedElements = document.querySelector("#boardId")
            var input2 = document.createElement('input');
            input2.setAttribute("type", "hidden");
            input2.setAttribute("name", "boardId");
            input2.setAttribute("value", selectedElements.value);

            form.appendChild(input1);
            form.appendChild(input2);
            console.log(form);
            document.body.appendChild(form);
            form.submit();
        }
    }
</script>
- 삭제 버튼에 boardFileId attribute를 줘서 클릭한 항목을 삭제할수 있게 하였습니다.

 

BoardController.java

package jpa.board.controller;

import jpa.board.dto.BoardDto;
import jpa.board.entity.Board;
import jpa.board.repository.BoardRepository;
import jpa.board.repository.CustomBoardRepository;
import jpa.board.service.BoardService;
import jpa.board.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import java.util.function.LongToIntFunction;

/**
 * packageName    : jpa.board.controller
 * fileName       : BoardController
 * author         : 김재성
 * date           : 2022-08-01
 * description    :
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022-08-01        김재성       최초 생성
 */
@Controller
@RequiredArgsConstructor
public class BoardController {

    private final CustomBoardRepository customBoardRepository;
    private final BoardService boardService;

    private final FileService fileService;

    /**
    * @methodName : list
    * @date : 2022-08-02 오후 2:07
    * @author : 김재성
    * @Description: 게시판 목록화면
    **/
    @GetMapping("/")
    public String list(String searchVal, Pageable pageable, Model model){
        Page<BoardDto> results = customBoardRepository.selectBoardList(searchVal, pageable);
        model.addAttribute("list", results);
        model.addAttribute("maxPage", 5);
        model.addAttribute("searchVal", searchVal);

        pageModelPut(results, model);
        return "board/list";
    }

    /**
    * @methodName : pageModelPut
    * @date : 2022-08-02 오후 4:36
    * @author : 김재성
    * @Description: pagenation 관련 값 model 넣기
    **/
    private void pageModelPut(Page<BoardDto> results, Model model){
        model.addAttribute("totalCount", results.getTotalElements());
        model.addAttribute("size",  results.getPageable().getPageSize());
        model.addAttribute("number",  results.getPageable().getPageNumber());
    }

    /**
    * @methodName : write
    * @date : 2022-08-02 오후 2:07
    * @author : 김재성
    * @Description: 게시판 글쓰기화면
    **/
    @GetMapping("/write")
    public String write(Model model){
        model.addAttribute("boardDto", new BoardDto());
        return "board/write";
    }

    /**
    * @methodName : save
    * @date : 2022-08-03 오후 2:15
    * @author : 김재성
    * @Description: 게시판 글 등록
    **/
    @PostMapping("/write")
    public String save(@Valid BoardDto boardDto, BindingResult result) throws Exception {

        //유효성검사 걸릴시
        if(result.hasErrors()){
            return "board/write";
        }

        boardService.saveBoard(boardDto);
        return "redirect:/";
    }

    /**
    * @methodName : update
    * @date : 2022-08-02 오후 2:07
    * @author : 김재성
    * @Description: 게시판 수정화면
    **/
    @GetMapping("/update/{boardId}")
    public String detail(@PathVariable Long boardId, Model model){
        Board board = boardService.selectBoardDetail(boardId);

        BoardDto boardDto = BoardDto.builder()
                            .id(boardId)
                            .title(board.getTitle())
                            .content(board.getContent())
                            .build();
        model.addAttribute("boardDto", boardDto);
        model.addAttribute("boardFile", customBoardRepository.selectBoardFileDetail(boardId));

        return "board/update";
    }

    /**
    * @methodName : update
    * @date : 2022-08-05 오전 11:20
    * @author : 김재성
    * @Description: 게시판 수정
    **/
    @PutMapping("/update/{boardId}")
    public String update(@Valid BoardDto boardDto, BindingResult result) throws Exception {
        //유효성검사 걸릴시
        if(result.hasErrors()){
            return "board/update";
        }

        boardService.saveBoard(boardDto);
        return "redirect:/";
    }

    /**
    * @methodName : delete
    * @date : 2022-08-05 오전 11:20
    * @author : 김재성
    * @Description: 게시판 목록 체크박스 선택 삭제
    **/
    @PostMapping("/delete")
    public String delete(@RequestParam List<String> boardIds){

        for(int i=0; i<boardIds.size(); i++){
            Long id = Long.valueOf(boardIds.get(i));
            boardService.deleteBoard(id);
        }

        return "redirect:/";
    }

    /**
    * @methodName : boardFileDelete
    * @date : 2022-08-10 오후 5:23
    * @author : 김재성
    * @Description: 파일삭제
    **/
    @PostMapping("/boardFileDelete")
    public String boardFileDelete(@RequestParam Long fileId, @RequestParam Long boardId){

        //게시판 파일삭제
        fileService.deleteBoardFile(fileId);

        return "redirect:/update/"+boardId;
    }
}
- boardFileDelete 부분을 추가하였습니다. 선택한 boardFileId를 업데이트하고, 상세화면으로 이동합니다.

 

FileService.java

package jpa.board.service;

import jpa.board.dto.BoardDto;
import jpa.board.dto.BoardFileDto;
import jpa.board.dto.FileDto;
import jpa.board.entity.Board;
import jpa.board.entity.BoardFile;
import jpa.board.entity.Member;
import jpa.board.repository.BoardFileRepository;
import jpa.board.repository.FileRepository;
import jpa.board.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.InputStream;
import java.util.*;

/**
 * packageName    : jpa.board.service
 * fileName       : FileService
 * author         : 김재성
 * date           : 2022-08-05
 * description    :
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022-08-05        김재성       최초 생성
 */

@Service
@RequiredArgsConstructor
@Slf4j
public class FileService {

    @Value("${upload.path}")
    private String uploadDir;

    private final FileRepository fileRepository;

    private final BoardFileRepository boardFileRepository;

    private final MemberRepository memberRepository;

    @Transactional
    public Map<String, Object> saveFile(BoardDto boardDto, Long boardId) throws Exception {
        List<MultipartFile> multipartFile = boardDto.getMultipartFile();

        //결과 Map
        Map<String, Object> result = new HashMap<String, Object>();

        //파일 시퀀스 리스트
        List<Long> fileIds = new ArrayList<Long>();

        try {
            if (multipartFile != null) {
                if (multipartFile.size() > 0 && !multipartFile.get(0).getOriginalFilename().equals("")) {
                    for (MultipartFile file1 : multipartFile) {
                        String originalFileName = file1.getOriginalFilename();    //오리지날 파일명
                        String extension = originalFileName.substring(originalFileName.lastIndexOf("."));    //파일 확장자
                        String savedFileName = UUID.randomUUID() + extension;    //저장될 파일 명

                        File targetFile = new File(uploadDir + savedFileName);

                        //초기값으로 fail 설정
                        result.put("result", "FAIL");

                        FileDto fileDto = FileDto.builder()
                                .originFileName(originalFileName)
                                .savedFileName(savedFileName)
                                .uploadDir(uploadDir)
                                .extension(extension)
                                .size(file1.getSize())
                                .contentType(file1.getContentType())
                                .build();
                        //파일 insert
                        jpa.board.entity.File file = fileDto.toEntity();
                        Long fileId = insertFile(file);
                        log.info("fileId={}", fileId);

                        try {
                            InputStream fileStream = file1.getInputStream();
                            FileUtils.copyInputStreamToFile(fileStream, targetFile); //파일 저장
                            //배열에 담기
                            fileIds.add(fileId);
                            result.put("fileIdxs", fileIds.toString());
                            result.put("result", "OK");
                        } catch (Exception e) {
                            //파일삭제
                            FileUtils.deleteQuietly(targetFile);    //저장된 현재 파일 삭제
                            e.printStackTrace();
                            result.put("result", "FAIL");
                            break;
                        }

                        BoardFileDto boardFileDto = BoardFileDto.builder()
                                .boardId(boardId)
                                .build();

                        BoardFile boardFile = boardFileDto.toEntity(file);
                        insertBoardFile(boardFile);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /** 파일 저장 db */
    @Transactional
    public Long insertFile(jpa.board.entity.File file) {
        return fileRepository.save(file).getId();
    }

    @Transactional
    public Long insertBoardFile(BoardFile boardFile) {
        return boardFileRepository.save(boardFile).getId();
    }

    /**
    * @methodName : deleteBoardFile
    * @date : 2022-08-10 오후 5:27
    * @author : 김재성
    * @Description: 게시판 파일 삭제
    **/
    @Transactional
    public BoardFile deleteBoardFile(Long boardFileId){
        BoardFile boardFile = boardFileRepository.findById(boardFileId).get();

        //삭제
        boardFile.delete("Y");
        return boardFile;
    }

}
- deleteBoardFile 메소드를 추가하였습니다. delYn을 Y로 업데이트 합니다.

 

BoardFile.java

package jpa.board.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;

/**
 * packageName    : jpa.board.entity
 * fileName       : BoardFile
 * author         : 김재성
 * date           : 2022-08-09
 * description    :
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022-08-09        김재성       최초 생성
 */

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@EntityListeners(AuditingEntityListener.class)
public class BoardFile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_file_id")
    private Long id;            //번호

    private Long boardId;
    private String delYn;

    @OneToOne
    @JoinColumn(name = "file_id")
    private File file;

    @Builder
    public BoardFile(Long boardId, Long fileId, String delYn, File file){
        this.boardId = boardId;
        this.delYn = "N";
        this.file = file;
    }

    public BoardFile delete(String delYn){
        this.delYn = delYn;
        return this;
    }
}

 

BoardRepositoryImpl.java

package jpa.board.repositoryImpl;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jpa.board.dto.BoardDto;

import jpa.board.dto.BoardFileDto;
import jpa.board.dto.QBoardDto;
import jpa.board.dto.QBoardFileDto;
import jpa.board.entity.BoardFile;
import jpa.board.repository.CustomBoardRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.stream.Collectors;

import static jpa.board.entity.QBoard.board;
import static jpa.board.entity.QBoardFile.boardFile;
import static jpa.board.entity.QMember.member;

/**
 * packageName    : jpa.board.repositoryImpl
 * fileName       : BoardRepositoryImpl
 * author         : 김재성
 * date           : 2022-08-02
 * description    :
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022-08-02        김재성       최초 생성
 */
@Repository
public class BoardRepositoryImpl implements CustomBoardRepository {
    private final JPAQueryFactory jpaQueryFactory;

    public BoardRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    /**
    * @methodName : selectBoardList
    * @date : 2022-08-10 오후 4:13
    * @author : 김재성
    * @Description: 게시판 목록
    **/
    @Override
    public Page<BoardDto> selectBoardList(String searchVal, Pageable pageable) {
        List<BoardDto> content = getBoardMemberDtos(searchVal, pageable);
        Long count = getCount(searchVal);
        return new PageImpl<>(content, pageable, count);
    }

    /**
    * @methodName : selectBoardDetail
    * @date : 2022-08-10 오후 4:13
    * @author : 김재성
    * @Description: 게시판 첨부파일 리스트
    **/
    @Override
    public List<BoardFileDto> selectBoardFileDetail(Long boardId) {
        List<BoardFileDto> content = jpaQueryFactory
                .select(new QBoardFileDto(
                         boardFile.id
                        ,boardFile.file.id
                        ,boardFile.file.originFileName
                        ,boardFile.file.size
                        ,boardFile.file.extension
                ))
                .from(boardFile)
                .leftJoin(boardFile.file)
                .where(boardFile.boardId.eq(boardId))
                .where(boardFile.delYn.eq("N"))
                .fetch();
        return content;
    }

    /**
    * @methodName : getCount
    * @date : 2022-08-10 오후 4:13
    * @author : 김재성
    * @Description: 게시판 페이징 count
    **/
    private Long getCount(String searchVal){
        Long count = jpaQueryFactory
                .select(board.count())
                .from(board)
                .where(containsSearch(searchVal))
                //.leftJoin(board.member, member)   //검색조건 최적화
                .fetchOne();
        return count;
    }

    /**
    * @methodName : getBoardMemberDtos
    * @date : 2022-08-10 오후 4:13
    * @author : 김재성
    * @Description: 게시판 페이징 목록
    **/
    private List<BoardDto> getBoardMemberDtos(String searchVal, Pageable pageable){
        List<BoardDto> content = jpaQueryFactory
                .select(new QBoardDto(
                        board.id
                        ,board.title
                        ,board.content
                        ,board.regDate
                        ,board.uptDate
                        ,board.viewCount
                        ,member.username))
                .from(board)
                .leftJoin(board.member, member)
                .where(containsSearch(searchVal))
                .where(board.delYn.eq("N"))
                .orderBy(board.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        return content;
    }

    /**
     * @methodName : containsSearch
     * @date : 2022-08-02 오후 5:28
     * @author : 김재성
     * @Description: %키워드% 조회
     **/
    private BooleanExpression containsSearch(String searchVal){
        return searchVal != null ? board.title.contains(searchVal) : null;
    }
}
- selectBoardFileDetail 부분에 boardFileId가 필요해서 추가하였습니다.

 

결과화면

 

삭제클릭시

 

삭제

 

- 다음장에는 수정 화면에서 파일 업로드시 파일이 추가되는것을 개발해보겠습니다.
복사했습니다!