Published 2022. 8. 10. 16:57
이전글 보기
https://aamoos.tistory.com/686
목표
- 저번장에서 파일업로드후 테이블에 데이터를 쌓고, 파일을 저장하는것까지 하였습니다. 이번장에서는 업로드한 파일을 상세에서 보여주고 다운로드를 할수있는것을 개발해보겠습니다.
첨부파일 영역 출력
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