파일 업로드 게시판 (첨부파일)

파일첨부 게시판에 대해 조금 더 알아보는 시간을 가지면서

이해한만큼 정리한 내용입니다. 코드로 배우는 스프링 웹프로젝트를 참고하였습니다.

파일첨부를 포함한 게시판은 구현할 때 파일첨부와 게시판 2개의 등록, 조회, 수정, 삭제등을 고려해야합니다. 보통은 게시판 글은 CRUD 특징을 가지고 있지만 파일첨부의 경우 등록, 조회, 삭제로만 이루어져 있습니다. (수정은 대게 파일을 다시 삭제 후 등록하는 로직으로 처리)

이러한 조건을 고려하여 처리하는 방법의 첫번째는 DB 테이블의 설계이며 실습한 게시판 테이블과 첨부파일 테이블은 다음과 같습니다.

1. 테이블

Board Table

스크린샷 2019-05-28 오후 10 37 04

Attach Table

스크린샷 2019-05-28 오후 10 36 44

Board 테이블의 PK는 bno, Attach 테이블의 PK는 uuid(중복 파일의 구분을 위한 컬럼), FK는Board 테이블의 게시물번호에 맞게 트랜잭션 처리하기 위해 Board의 bno를 참조하는 bno 컬럼을 설정하였습니다.

2. VO 객체 생성

AttachVO.java

1
2
3
4
5
6
7
8
@Data
public class AttachVO {
	private String uuid;
	private String uploadPath;
	private String fileName;
	private boolean fileType;
	private Long bno;
}

BoardDTO.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Data
public class BoardDTO {
	private long bno;
	private String title;
	private String content;
	private String writer;
	private Date regDate;
	private Date updateDate;
	//첨부파일 VO 객체 리스트
	private List<BoardAttachVO> attachList;
	
}

3. 첨부파일 처리 (입력/삭제)


3-1. 입력

파일업로드시 jquery를 통해 selector를 활용하여 submit 처리를 하였습니다.

formObj를 통해 submit을 눌렀을때 현재 화면(Ajax 통신 후 li 태그로 스크립트 처리)에 파일업로드된 리스트의 객체들을 불러와 객체를 input type="hidden" 으로 name값은 지정한 AttachVO 필드명으로 value는 li태그에 data 속성명으로 접근하여 처리합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 	var formObj = $("form[role='form']");
  
  $("button[type='submit']").on("click", function(e){
    //submit 처리 취소
    e.preventDefault();
    
    var str = "";
    
    //객체의 bno, fileName, type, uploadpath, uuid 등 attach 객체 정보를 전송
    $(".uploadResult ul li").each(function(i, obj){
      
      var attachObj = $(obj);

      str += "<input type='hidden' name='attachList["+i+"].fileName' value='"+attachObj.data("filename")+"'>";
      str += "<input type='hidden' name='attachList["+i+"].uuid' value='"+attachObj.data("uuid")+"'>";
      str += "<input type='hidden' name='attachList["+i+"].uploadPath' value='"+attachObj.data("path")+"'>";
      str += "<input type='hidden' name='attachList["+i+"].fileType' value='"+ attachObj.data("type")+"'>";
      
    });
    
    console.log(str);
    //전송
    formObj.append(str).submit();
    
  });

해당 submit은 다음과 같은 form태그로 인해 Controller에 전송됩니다.

1
<form role="form" action="/board/register" method="post">

BoardController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@PostMapping("/register")
public String register(BoardDTO board, RedirectAttributes rttr) {
  log.info("/register Board : "+board);

  //파일첨부 확인
  if(board.getAttachList() != null) {
    board.getAttachList().forEach(attach ->log.info(attach));
  }

  service.register(board);
  rttr.addFlashAttribute("result",board.getBno());
  log.info("---------/register----------");
  return "redirect:/board/list";
}

해당 컨트롤러에서 board의 객체를 받아 비지니스로직처리를 위해 service로 넘기며 현재 board객체엔 게시물과 private List<BoardAttachVO> attachList 의 객체가 함께 들어있으며 조건문으로 현재의 파일첨부가 있는지 log를 통해 확인할 수 있습니다.

BoardServiceImpl.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired
@Setter
private BoardMapper mapper;

@Autowired
@Setter
private BoardAttachMapper attachMapper;

//insert
@Transactional
@Override
public void register(BoardDTO board) {
  //게시글 먼저 입력 후
  mapper.insertSelectKey(board);
  //첨부파일 없을때 처리
  if(board.getAttachList() == null || board.getAttachList().size() <= 0 ) {
    return;
  }
  //첨부파일 있으면 처리
  board.getAttachList().forEach(attach -> {

    attach.setBno(board.getBno()); attachMapper.insert(attach);

  });
}

게시물 등록을 위한 mapper와 파일첨부 등록을 위한 attachMapper를 @Autowired로 빈을 자동으로 주입시킴과 동시에 트랜잭션 처리시 넘겨줄 매개변수를 위해 @Setter를 선언합니다. 또한 트랜잭션의 관리를 위해 @Transactional까지 선언해줍니다.

insertSelectKey(board) 는 게시물 등록 후 파일 업로드나 댓글처리를 위해 PK(bno)를 찾기 위한 selectKey 쿼리 메소드입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<insert id="insert">
  insert into tbl_board (bno, title, content, writer)
  values (seq_board.nextval, #{title}, #{content}, #{writer})
</insert>
		
<insert id="insertSelectKey">
  <selectKey keyProperty="bno" order="BEFORE" resultType="long">
    select seq_board.nextval from dual
  </selectKey>
  insert into tbl_board(bno, title, content, writer)
  values (#{bno}, #{title}, #{content}, #{writer})		
</insert>

게시물이 등록된 후 첨부파일이 없을때의 조건문 처리 후 게시물이 없으면 false를 리턴 있다면 attach.setBno(board.getBno()) 로 AttachVO의 객체에 bno 변수에 selectKey 로 받아온 board의 bno를 넘겨주고 attachMapper.insert(attach) 를 작성하여 트랜잭션 처리를 하여 게시물을 등록하게 됩니다.

결과 화면처리

등록

스크린샷 2019-05-29 오전 2 39 08

게시물 보기

스크린샷 2019-05-29 오전 2 42 02

DB 확인 및 파일 업로드 확인

스크린샷 2019-05-29 오전 2 43 25

스크린샷 2019-05-29 오전 2 39 50

스크린샷 2019-05-29 오전 2 42 32

3-2. 삭제

업로드한 게시물의 삭제는 폴더에 생성한 파일을 삭제하는 것과 함께 더불어 DB table의 게시글과 파일첨부 데이터까지 제거해야 합니다.

해당 작업처리 순서는 다음과 같이 진행하였습니다.

  • Board 게시물 삭제
  • 파일 Attach DB 삭제
    • 해당 게시물의 첨부파일 목록 필요
    • 이미지파일은 썸네일이 생성되어 있어 추가 삭제 필요
1
2
3
<delete id="deleteAll">
	delete tbl_attach where bno = #{bno} 
</delete>

게시물을 삭제시 해당글의 전체 파일을 삭제해줘야하기때문에 게시물의 bno로 조건을 설정하고 ServiceImpl에서는 첨부된 파일을 먼저 삭제 후 게시글을 삭제합니다.

해당 게시물은 현재 tbl_attach bno 컬럼이 tbl_board bno컬럼을 참조하고 있기 때문에

참조키 테이블의 데이터를 먼저 제거 후 tbl_board 테이블의 게시물을 제거해야합니다. 이를 참조 무결성이라 부릅니다.

BoardServiceImpl.java

1
2
3
4
5
6
7
@Override
	public boolean remove(Long bno) {
    //파일 첨부 선 제거
		attachMapper.deleteAll(bno);
    //게시물 제거
		return mapper.delete(bno) == 1;
	}

BoardController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@PostMapping("/delete")
	public String remove(@RequestParam("bno")Long bno, RedirectAttributes rttr, @ModelAttribute("cri") Criteria cri) {
		
		//게시물의 bno로 해당 첨부파일 리스트 가져오기
		List<BoardAttachVO> attachList = service.getAttachList(bno);
		
    //DB의 데이터가 제거되었을때 처리
		if(service.remove(bno)) {
      //파일 제거 메소드
			deleteFiles(attachList);
			rttr.addFlashAttribute("result","success");
		}
		
		rttr.addAttribute("pageNum",cri.getPageNum());
		rttr.addAttribute("amount",cri.getAmount());
		rttr.addAttribute("type",cri.getType());
		rttr.addAttribute("keyword",cri.getKeyword());
		
		return "redirect:/board/list" + cri.getListLink();
	}	
	

//게시물 삭제 후 첨부파일 삭제
private void deleteFiles(List<BoardAttachVO> attachList) {

  if(attachList == null || attachList.size() == 0) {
    return;
  }

  log.info("delete attach files" + attachList);

  attachList.forEach(attach -> {

    File file;

    try {        
      String filePath  = attach.getUploadPath()+"/" + attach.getUuid()+"_"+ attach.getFileName();

      log.info("filePath -----> : " + filePath);
     
      file = new File(URLDecoder.decode(filePath,"UTF-8"));

      Magic magic = new Magic();
      MagicMatch match = magic.getMagicMatch(file,false);
      String mime = match.getMimeType();
      log.info("mime type : " + mime);

      file.delete();

      if(mime.contains("image")) {

        String thumbnailPath= attach.getUploadPath()+"/s_" + attach.getUuid()+"_"+ attach.getFileName();
        log.info("thumbnailPath Name : " + thumbnailPath);

        file = new File(URLDecoder.decode(thumbnailPath,"UTF-8"));
        file.delete();
      }

    }catch(Exception e) {
      log.error("delete file error" + e.getMessage());
    }
    
  });
}

저장된 첨부파일을 삭제시 List<BoardAttachVO> attachList = service.getAttachList(bno) 를 이용해 파일리스트를 받아온 후 파일 삭제 메소드를 작성하였습니다.

File 객체를 사용하여 attachList 객체에 저장된 Path를 인스턴스를 생성합니다. 이후 파일을 삭제하는 file.delete() 선언하고 이미지파일의 경우 썸네일이 추가적으로 생성되었기에 이전 포스팅에 작성한 mime체크 를 참조하여 MIME-TYPE을 확인 후 썸네일 파일의 path를 이용해 추가적으로 file.delete() 처리를 완료합니다.

결과

폴더

스크린샷 2019-05-29 오전 3 18 31

게시물 리스트

스크린샷 2019-05-29 오전 3 18 40

tbl_board

스크린샷 2019-05-29 오전 3 18 56

tbl_attach

스크린샷 2019-05-29 오전 3 18 47


전체적으로 파일업로드에 대한 화면 처리는 ajax를 활용하고 MVC패턴을 이용해 DB까지 저장하는 과정을 학습하며 낯설기만 했던 ajax와 메소드의 활용, 그리고 자바스크립트와 제이쿼리의 선택자에 대해 조금은 익숙해질 수 있는 기회였습니다. 실습예제를 통해 mac과 java8 버전에서 구현되지 않았던 부분도 Magic, Match 라이브러리를 통해 직접 변경하여 파일 삭제와 썸네일 파일 삭제를 처리하였습니다.


Reference