vue + jpa project (13) - 게시판 첨부파일 처리(이미지) 본문

프로그램/Vue.js

vue + jpa project (13) - 게시판 첨부파일 처리(이미지)

반응형

게시판에서 첨부를 할 수 있는 기능을 적용시켜 보겠다. 

사실 실제 프로젝트에서는 멀티로 다양하게 첨부할 수 있는 기능을 만들겠지만 

여기서는 맛보기?로 간단하게 처리할 수 있게만 만들려고 한다. 

이것을 가지고 보다 다양한 방식으로 확장해나가면 좋을 듯 한다. (사실 많이 바꾸기도 해야한다 ^^;;)

 

1. 우선 application.yml에 첨부와 관련된 내용을 아래와 같이 추가한다. 

    그리고 파일 경로 폴더가 없다며 미리 생성을 시키도록 한다. (백엔드에서 폴더를 자동생성이 아닌경우)

spring:		# 이 줄은 위에 이미 정의되어 있어서 추가시에는 생략해도 된다.
  servlet: 
    multipart:
      max-file-size: 8MB        # 첨부가능한 파일사이즈
      max-request-size: 8MB     # request로 요청가능한 사이즈
      
file:							# file 앞에 공백이 없어야 함
  upload-path: c:/temp/upload/  # 임의 정의한 것으로 파일 업로드 경로로 사용

 

2. 그리고 BoardDto.java 파일에 MultipartFile picture 항목을 추가한다.

    private Long boardNo;
    private String title;
    private String writer;
    private String content;
    private String pictureUrl;
    private MultipartFile picture;    // 첨부파일용 항목
    private String regDate;
    private String updDate;

 

3. BoardController.java 파일에 첨부파일 관련한 부분을 추가한다. 

   첨부파일을 업로드 받으면서 저장까지 처리를 해야하므로 별도 메소드를 추가해서 

   기존의 post와 patch처리를 하나로 처리하도록 하겠다.

   

   하나씩 BoardController에 추가를 하면, 우선 로그를 찍기 위해서 @Slf4j 어노테이션을 추가하고, 상단에 첨부파일
   올라가는 경로를 @Value 어노테이션을 이용하여  값을 읽어와서 저장시킨다.

@Slf4j
@RequiredArgsConstructor
@CrossOrigin
@RestController
public class BoardController {
	
	@Value("${file.upload-path}")
	private String file_upload_path;

delete 메소드 다음에 아래 메소드를 추가하고 기존 @PostMapping과 @PatchMapping 의 URI를 다른 것으로 바꾼다.

나의 경우에는 "board_old" 로 둘 다 바꾸었다. 첨부파일에 해당하는 Content-type이 multipart/form-data 경우에는 

@PatchMapping을 사용하지 못하고 @PostMapping 만 사용가능하다. 

그리고 데이터를 받을 때 @ModelAttribute로 받아야 multipart/form-data 를 받을 수 있게 된다.

나중에 아래서 프론트 단에서 넘겨주는 데이터는 기존의 boardDto에 각각 들어가고 

위에서 BoardDto 에 추가한 picture 항목은 파일데이터를 받게된다.

중간에 파일 처리는 파일이 있는 경우에는 uploadFile 메소드를 통해서 별도 파일을 처리하고 

그 이름만 넘겨받아서 테이블에 저장하도록 한다. 

또한 이 메소드 하나로 등록과 수정을 동시에 처리해야하므로 boardNo 값이 없으면 저장, 있으면 수정하도록 하였다.

    /* 이미지 첨부 및 저장/수정 */
    @PostMapping("/board")
    public BoardEntity register(@ModelAttribute BoardDto boardDto) throws Exception {
        
        MultipartFile pictureFile = boardDto.getPicture();
        
        if(pictureFile != null) {
            log.info("register pictureFile != null " + pictureFile.getOriginalFilename());
            
            String createdPictureFilename = uploadFile(pictureFile.getOriginalFilename(), pictureFile.getBytes());
            boardDto.setPictureUrl(createdPictureFilename);
        }
        else {
            log.info("register pictureFile == null ");
        }
        
        if(boardDto.getBoardNo() == null) {
            return boardService.create(boardDto);
        } else {
            return boardService.update(boardDto);
        }
    }

 

위의 uploadFile 메소드를 아래와 같이 추가한다. 원래의 파일명 앞에 UUID를 붙혀서 폴더에 저장시킨다.

UUID를 붙히는 이유는 중복파일을 업로드 할 수 있기 때문이다.

(실제로는 이렇게 저장하지 않고 파일명자체를 UUID로 바꾸고 별도 첨부파일 테이블에서 관리한다)

 

    private String uploadFile(String originalName, byte[] fileData) throws Exception {
        UUID uid = UUID.randomUUID();
        String createdFileName = uid.toString() + "_" + originalName;
        File target = new File(file_upload_path, createdFileName);
        FileCopyUtils.copy(fileData, target);
        return createdFileName;
    }

 

그리고 프론트에서 저장된 이미지를 조회하여 보여주는 부분도 필요하기 때문에 아래의 메소드를 추가한다.

추후에 /board/picture/ 라는 URI에 Get파라미터로 boardNo값을 수신하여 저장된 정보를 가져와서

파일을 리턴해주는 메소드이고, 미디어타입이 일단 jpg, gif, png만 처리되도록 한다. 

    @GetMapping("/board/picture")
    public Resource displayFile(@RequestParam("boardNo") Long boardNo) throws Exception {
        
        String pictureUrl = boardService.getPictureUrl(boardNo);
        
        try {
            String formatName = pictureUrl.substring(pictureUrl.lastIndexOf(".") + 1);
            
            MediaType mediaType = getMediaType(formatName);
            
            if(mediaType != null) {
                return new UrlResource("file:" + file_upload_path + pictureUrl);
            } else {
                return null;
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    private MediaType getMediaType(String formatName){
        if(formatName != null) {
            if(formatName.equalsIgnoreCase("JPG")) {
                return MediaType.IMAGE_JPEG;
            }
            if(formatName.equalsIgnoreCase("GIF")) {
                return MediaType.IMAGE_GIF;
            }
            if(formatName.equalsIgnoreCase("PNG")) {
                return MediaType.IMAGE_PNG;
            }
        }
        return null;
    }

 

4. 프론트의 BoardWrite.vue 파일을 아래와 같이 수정한다.

    boardWrite.vue

...  중략
    <div class="board-content">
      <textarea id="" cols="30" rows="10" v-model="content" class="w3-input w3-border" style="resize: none;">
      </textarea>
    </div>
    <!-- 아래 div 추가 -->
    <div class="board-content">
      <input type="file" @change="handleFileChange($event)" >
    </div>
... 중략


<script>
export default {

  data() {
    return {
      requestBody : this.$route.query,
      boardNo : this.$route.query.boardNo,

      title : '',
      content : '',
      writer : '',
      picture : '',	// 첨부파일 항목 추가
      reg_date : '',
    }
  }, 
... 중략

    fnSave() {

      if(!confirm('저장하시겠습니까?')) return

      let apiUrl = this.$serverUrl + '/board'
      this.form = {
        "boardNo": this.boardNo,
        "title": this.title,
        "writer": this.writer,
        "content": this.content,
        "picture": (this.picture === '')?null:this.picture,	// 첨부파일 항목 추가
      }

      if (this.boardNo === undefined) {
        //INSERT
        this.$axios.post(apiUrl, this.form, {
            headers: {'Content-type': 'multipart/form-data'}	// 첨부파일 헤더추가
          })
          .then((res) => {
            alert('정상적으로 저장되었습니다.')
            this.fnView(res.data.board_no);
          })
          .catch((err) => {
            if (err.message.indexOf('Network Error') > -1) {
              alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
            }
          })

      } else {
        //UPDATE
        this.$axios.post(apiUrl, this.form, {                   // 수정도 .post로 변경
            headers: {'Content-type': 'multipart/form-data'}    // 첨부파일 헤더추가
          })
          .then((res) => {
            alert('정상적으로 저장되었습니다.')
            this.fnView(res.data.board_no);
          })
          .catch((err) => {
            if (err.message.indexOf('Network Error') > -1) {
              alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
            }
          })
      }
    },

    handleFileChange(evt) {  // 첨부파일 처리 함수 추가
      this.picture = evt.target.files[0]
    }
... 중략

 

5. 프론트의 BoardDetail.vue 파일을 아래와 같이 수정한다.

    boardDetail.vue

...  중략
    <div class="board-content">
      <span>{{ content }}</span>
    </div>
    <!-- 아래 div 추가 -->
    <div class="board-content">
      <img v-if="picture_url" :src="previewPicture(`${boardNo}`)" width="220" height="220">
    </div>
... 중략


<script>
export default {

  data() {
    return {
      requestBody : this.$route.query,
      boardNo : this.$route.query.boardNo,

      title : '',
      content : '',
      writer : '',
      picture_url : '',    // 첨부파일 항목 추가
      reg_date : '',
    }
  }, 

... 중략
    fnGetView() {
      this.$axios.get(this.$serverUrl + '/board/' + this.boardNo, {
        params : this.requestBody,
      }).then((res) => {
        this.title = res.data.title
        this.writer = res.data.writer
        this.content = res.data.content
        this.picture_url = res.data.picture_url	// 첨부파일 항목 추가
        this.reg_date = res.data.reg_date
      })
      .catch((err) => {
        if (err.message.indexOf('Network Error') > -1) {
          alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
        }
      })
    },

... 중략
    previewPicture(boardNo) {
      return 'http://localhost:8081/board/picture?boardNo=' + boardNo + '&timestamp=' + new Date().getTime()
    }

 

이렇게 수정하고 신규로 등록하여 첨부파일을 지정하고 저장, 수정을 진행해보자.

 

 

 

게시판에 대한 파일첨부를 할 수 있는 기능을 어느정도? 완료하였다. (완벽하진 않다는 뜻임)

게시판 관련한 기능을 이것으로써 마무리가 되었다. 

다음 장에서는 로그인과 관련한 처리를 진행해보겠다. 

반응형

프로그램/Vue.js Related Articles

MORE

Comments