이전글 보기

https://aamoos.tistory.com/686

 

[Spring Jpa] 15. 게시판 만들기 - 게시판 파일 참조 테이블 생성, 데이터 추가

이전글 보기 https://aamoos.tistory.com/685 [Spring Jpa] 14. 게시판 만들기 - 멀티파일 업로드 목표 - 파일을 여러개 올릴수 있는 input을 추가한후 멀티파일 업로드 기능을 구현하려고 합니다. 이번장에서는

aamoos.tistory.com

 

목표

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

 

첨부파일 영역 출력

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" 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>
          </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>
</script>
- 첨부파일 영역을 추가하였습니다.

 

BoardController.java

/**
    * @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";
    }
- 수정화면 api에서 selectBoardFileDetail을 추가하였습니다. 해당 repository는 querydsl을 이용해서 file 테이블, boardFile테이블을 조인해서 데이터를 조회 합니다.

 

CustomBoardRepository.java

package jpa.board.repository;

import jpa.board.dto.BoardDto;
import jpa.board.dto.BoardFileDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

/**
 * packageName    : jpa.board.repository
 * fileName       : CustomBoardRepository
 * author         : 김재성
 * date           : 2022-08-02
 * description    :
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022-08-02        김재성       최초 생성
 */
public interface CustomBoardRepository {

    /**
    * @methodName : selectBoardList
    * @date : 2022-08-10 오후 3:46
    * @author : 김재성
    * @Description: 게시판 페이징 목록
    **/
    Page<BoardDto> selectBoardList(String searchVal, Pageable pageable);

    /**
    * @methodName : selectBoardDetail
    * @date : 2022-08-10 오후 3:46
    * @author : 김재성
    * @Description: 게시판 상세 첨부파일 조회
    **/
    List<BoardFileDto> selectBoardFileDetail(Long boardId);
}

 

BoardRepositoryServiceImpl.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.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 메소드가 추가 하였습니다. boardFile과 file을 조인해서 파일 정보를 가져옵니다.

 

결과화면

 

파일 다운로드

FileController.java

package jpa.board.controller;

import jpa.board.entity.File;
import jpa.board.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Path;
import java.nio.file.Paths;

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

@Controller
@RequiredArgsConstructor
public class FileController {

    private final FileRepository fileRepository;

    @GetMapping(value = {"/fileDownload/{fileIdx}"})
    @ResponseBody
    public void downloadFile(HttpServletResponse res, @PathVariable Long fileIdx) throws UnsupportedEncodingException {

        //파일 조회
        File fileInfo = fileRepository.findById(fileIdx).get();

        //파일 경로
        Path saveFilePath = Paths.get(fileInfo.getUploadDir() + java.io.File.separator + fileInfo.getSavedFileName());

        //해당 경로에 파일이 없으면
        if(!saveFilePath.toFile().exists()) {
            throw new RuntimeException("file not found");
        }

        //파일 헤더 설정
        setFileHeader(res, fileInfo);

        //파일 복사
        fileCopy(res, saveFilePath);
    }

    /**
     * 파일 header 설정
     * @param res
     * @param fileInfo
     * @throws UnsupportedEncodingException
     */
    private void setFileHeader(HttpServletResponse res, File fileInfo) throws UnsupportedEncodingException {
        res.setHeader("Content-Disposition", "attachment; filename=\"" +  URLEncoder.encode((String) fileInfo.getOriginFileName(), "UTF-8") + "\";");
        res.setHeader("Content-Transfer-Encoding", "binary");
        res.setHeader("Content-Type", "application/download; utf-8");
        res.setHeader("Pragma", "no-cache;");
        res.setHeader("Expires", "-1;");
    }

    /**
     * 파일 복사
     * @param res
     * @param saveFilePath
     */
    private void fileCopy(HttpServletResponse res, Path saveFilePath) {
        FileInputStream fis = null;

        try {
            fis = new FileInputStream(saveFilePath.toFile());
            FileCopyUtils.copy(fis, res.getOutputStream());
            res.getOutputStream().flush();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
        finally {
            try {
                fis.close();
            }
            catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}
- /filedownload api에 파일 id를 날리면 file이 다운로드 되는 api 입니다.

 

결과화면

 

- 클릭시 해당 파일이 정상적으로 다운로드 되었습니다. 다음장에는 파일 삭제 기능을 개발해보겠습니다. 

 

다음글 보기

https://aamoos.tistory.com/689

 

[Spring Jpa] 17. 게시판 만들기 - 첨부파일 삭제

목표 - 이번장에서는 첨부파일 삭제버튼을 클릭하면 첨부파일 DEL_YN을 Y로 업데이트 하는부분을 개발해보겠습니다. update.html 제목 Incorrect date 작성자 내용 첨부파일 파일이름1.png 삭제 수정 목록

aamoos.tistory.com

 

복사했습니다!