Published 2022. 8. 10. 18:01
이전글 보기
https://aamoos.tistory.com/688
목표
- 이번장에서는 첨부파일 삭제버튼을 클릭하면 첨부파일 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가 필요해서 추가하였습니다.
결과화면
삭제클릭시
삭제
- 다음장에는 수정 화면에서 파일 업로드시 파일이 추가되는것을 개발해보겠습니다.