상세 컨텐츠

본문 제목

[SPRING] MultipartFile 게시판 이미지 업로드(2)ajax 이용과 게시판에 적용

JAVA/SPRING

by ranlan 2022. 1. 10. 16:18

본문

728x90

기존의 게시판 CRUD 👉🏻 2021.03.21 - [java/spring] - [JPA] 게시판 CRUD

 

[JPA] 게시판 CRUD

가장 먼저 간단한 게시판 기능을 구현하기로 하였다. 카테고리를 선택하고 글의 제목과 내용을 입력하여 게시물을 등록하고 조회, 수정, 삭제까지 할 수 있도록 하였다. 그 외의 이미지, 동영상

juran-devblog.tistory.com

 

* 일단 게시판 제목과 내용은 Board에, 이미지 관련은 Images에 따로 저장하도록 하였는데 연관관계 매핑을 신경써서 좀 더 수정해야할 것 같다.

 

 

기존의 게시판 작성 기능에 이미지 업로드 기능 백엔드 개발

1. Images 엔티티 작성

📦 Images

@NoArgsConstructor
@Entity
@Getter
@Setter
public class Images extends DateEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long imageNo;

    @Column(length = 500, nullable = false)
    private String originImageName;

    @Column(length = 500, nullable = false)
    private String imageName;

    @Column(length = 1000, nullable = false)
    private String imagePath;

    @Builder
    public Images(String originImageName, String imageName, String imagePath) {
        this.originImageName = originImageName;
        this.imageName = imageName;
        this.imagePath = imagePath;
    }
}
  • Images 테이블 primary key
  • 원래 이미지 파일 명
  • 새로 부여한 이미지 파일 명
  • 이미지 저장 경로

 

2. 기존의 Request 객체 수정

📄 BoardRequest (게시판 작성 form을 받아오는 객체)

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BoardRequest {

    @NotNull
    private String boardTitle;
    
    @NotNull
    private String boardContent;
    
    @NotNull
    private Long categoryNo;
    
    @NotNull
    private MultipartFile image; // 새로 추가

}

 

3. 레퍼지토리 작성

📄 ImageRepository

public interface ImageRepository extends JpaRepository<Images, Long> {
}

JpaRepository 상속

 

4. 서비스 작성

📄 ImageService

@Service
@RequiredArgsConstructor
public class ImageService {

    private final ImageRepository imageRepository;

    @Transactional
    public void saveImage(MultipartFile mtf) {

        LocalDateTime now = LocalDateTime.now();
        int year = now.getYear();
        int month = now.getMonthValue();
        int day = now.getDayOfMonth();
        int hour = now.getHour();
        int minute = now.getMinute();
        int second = now.getSecond();
        int millis = now.get(ChronoField.MILLI_OF_SECOND);

        String absolutePath = new File("/Users/juran/Desktop").getAbsolutePath() + "/";
        String newFileName = "image" + hour + minute + second + millis;
        String fileExtension = '.' + mtf.getOriginalFilename().replaceAll("^.*\\.(.*)$", "$1"); // 정규식 이용하여 확장자만 추출
        String path = "images/local/" + year + "/" + month + "/" + day;

        try {
            File file = new File(absolutePath + path);
            if(!file.exists()) {
                file.mkdirs();
            }

            file = new File(absolutePath + path + "/" + newFileName + fileExtension);
            mtf.transferTo(file);

            Images images = Images.builder()
                    .originImageName(mtf.getOriginalFilename())
                    .imageName(newFileName + fileExtension)
                    .imagePath(path)
                    .build();

            imageRepository.save(images);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
  • 파일 저장 경로와 폴더 생성, 새로운 파일 명 부여는 모두 이전 포스팅과 동일
  • 다른 점은 컨트롤러가 아닌 서비스단에서 이뤄지는 작업임으로 ImageDto를 생성하지 않고 바로 객체를 생성하여 DB에 저장
  • 컨트롤러에서 HTTP Request 중 파일만 ImageService로 전달하여 저장할 예정

 

5. 기존의 컨트롤러 수정

📄 BoardApiController

@PostMapping(path = "register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<?> register(@ModelAttribute BoardRequest boardRequest, Authentication authentication) throws Exception {
    
        Member member = memberService.loadUserByUsername(authentication.getPrincipal().toString());
        Category category = categoryService.findByCategoryNo(boardRequest.getCategoryNo());
        boardService.registerBoard(boardRequest.getBoardTitle(), boardRequest.getBoardContent(), member, category);
        imageService.saveImage(boardRequest.getImage()); // 이미지 저장 부분 새로 추가
        
        ApiResponse apiResponse = new ApiResponse(true, "게시물 등록");
        return ResponseEntity.ok(apiResponse);
    }
  • @RequestBody에서 @ModelAttribute로 수정
  • ImageService.saveImage 호출하여 이미지 저장

 

 

이미지 업로드 관련하여 JSP 및 AJAX 수정

6. JSP 수정

📄 register.jsp

<form name="form" method="post" enctype="multipart/form-data" id="frmData">
  <input class="form-control" type="file" name="image" multiple="multiple"/>
</form>

파일 선택하는 부분 추가

 

7. 스크립트 수정

$('#btnBoarddRegister').on({ // 등록 버튼 눌렀을 때
    click: function () {
        // 폼 데이터 생성
        let formData = new FormData($("#frmData")[0]);
        formData.append("boardTitle", $('#boardTitle').val());
        formData.append("boardContent", $('#boardContent').val());
        formData.append("categoryNo", $('#selectCategory option:selected').val());

        let boardRegConfirm = confirm('등록하시겠습니까?');
        if(boardRegConfirm == true) {
            $.ajax({
                url: baseUrl + '/api/board/register',
                type: 'POST',
                enctype: 'multipart/form-data',
                contentType: false,
                processData: false,
                data: formData,
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("${_csrf.headerName}", "${_csrf.token}");
                },
                success: function (result) {
                    location.href = '/';
                }, error: function (error) {
                    console.log(error)
                }
            });
        }
    }
});

* FormData

ajax로 폼데이터 전송을 가능하게 해주는 객체로 form 필드와 그 값을 나타내는 일련의 key/value 쌍을 생성

 

 

두둥 이미지 업로드 실행 결과

register.jsp

업로드한 이미지 미리보기는 추가할 예정.. 곧..

 

Images

자꾸 보이는 허점들.. 저장 시간도 추가해야함.. 어쨌든 저장 성공..!!!

 

 

날 힘들게한 오류들

처음에 그냥 multipartfile만 request 객체에 추가하고 스크립트에는 formData으로만 수정해줬을 때 발생한 오류1

data: formData

Uncaught TypeError: Illegal invocation

 

ajax로 데이터를 보낼 때 파라미터 값이 정상적으로 들어가지 않았을 때 발생하는 오류라고 한다.

 

json으로 보내지 않아서 생긴 오류인줄 알고

data: JSON.stringfy(formData)

로 수정하였다.

 

그랬더니 이번에는 백단에서 발생한 오류2

java.lang.NullPointerException

 

BoardRequest로 받은 객체에서 get으로 값을 조회하려 하니 저렇게 널포인터에러가 뜬다.

이상하다.. Request로 바로 못 받는거 같아서 이리저리 찾다 발견한 것이 MultipartHttpServletRequest

* 참고로 Multipart는 HTTP 요청 시 DTO로 받을 수 있다고 함

public ResponseEntity<?> register(@RequestBody MultipartHttpServletRequest mtfRequest, Authentication authentication) throws Exception {

    // FormData 사용시 한글이 깨지는 것을 방지
    String[] boardTitle = mtfRequest.getParameterValues("boardTitle"); 
    String[] boardContent = mtfRequest.getParameterValues("boardContent");
    String[] categoryNo = mtfRequest.getParameterValues("categoryNo");
    String convertToBoardTitle = new String(boardTitle[0].getBytes("8859_1"),"utf-8");
    String convertToBoardContent = new String(boardContent[0].getBytes("8859_1"),"utf-8");


    Iterator<String> its = mtfRequest.getFileNames();
    while (its.hasNext()) {
        String fileNm = (String) its.next();
        MultipartFile mtf = mtfRequest.getFile(fileNm);
        imageService.saveImage(mtf);
    }
    
    ....

바로 get으로 하나의 MultipartFile을 못꺼내오고 이름을 먼저 추출하여 그걸로 file을 추출해야 한다기에 코드를 이렇게 수정해주었고

한글이 깨지는 문제가 발생하다고 하여 위와 같이 "utf-8"로 변환해주었다.

 

이 방법도 안되서 다시 이전에 Request 객체로 받는 방법으로 다시 수정했다.

 

뭐 어찌저찌 머리를 싸매다 마주친 오류3(는 아니고 그냥 경고 로그)

 

org.springframework.web.HttpMediaTypeNotSupportedException

 

이거 보고 백단이 문제가 아니라 요청 보낼 때, 스크립트에서 ajax로 요청이 잘 못가는거 같아 multipartfile ajax 하는 스크립트를 열심히 찾아보았다.

 

enctype: 'multipart/form-data',
contentType: false,
processData: false,
data: formData,
  1. contentType 속성 수정
    일단 기존에 contentType: 'application/json'을 없애고 false로 변경 (이거 바꿀 때마다 스크립트 오류 로그가 달라짐)
  2. processData 속성 추가
    processData의 속성 기본값은 true로 ajax 통신으로 데이터를 전송할 때 key: value값을 query string으로 변환해서 보낸다.
    이 값을 false로 바꾸면 query string으로 변환하지 않으며 파일 전송 시에 사용한다고 한다.
  3. enctype 속성 추가
    form 태그 안에 enctype 속성이 있기 때문에 필요 없을 줄 알았는데 내가 submit으로 폼 전송하는 것이 아니기 때문에 추가해주었다.
  4. 추가했던 JSON.stringfy() 다시 삭제해줌
    기껏 위에 속성 다 수정하고 JSON.stringfy() 제거 안해서 시간 오래 잡아먹음 ㅠ

 

그리하여 성공!! 이미지 미리보기랑 조회, 수정, Board테이블이랑 매핑 등등,, 아직 해야할 게 많군요

 

728x90

관련글 더보기

댓글 영역