2022-03-28(월)

AJAX에서 하느냐 웹브라우저에서 하느냐

16.1 파일 업로드 : 자바스크립트 빌트인 객체 이용하기

http://localhost:8080/book/index.html

목록에 책 사진이 보인다
하나의 책을 선택하면 더 큰 그림으로 보인다
사진을 클릭하면 더 큰 사진이 나온다

도서록 테이블에 책 사진 파일 이름을 저장할 컬럼을 추가한다.

alter table ml_book
  add column photo varchar(255);

도메인 클래스(값 객체; Value Object; VO)에 책 사진 파일 이름을 저장할 필드를 추가한다.

3단계 - SQL Mapper 파일을 변경한다.
resultMap 태그에 photo 컬럼 매핑 정보 추가

join 데이터는 생략하면 안 됨..
헷갈리니까 필드 이름이랑 컬럼 이름이 같아도 그냥 다 적기

findAll, findByNo, insert, update SQL문에 photo 컬럼 추가

http://localhost:8080/book/index.html

4단계 - 사진 파일 업로드 기능을 페이지 컨트롤러에 추가한다.
BookController 클래스 변경

서비스랑 Dao는 바꿀 필요 없음

  @RequestMapping("/book/add")
  public Object add(Book book, MultipartFile file) throws Exception {

    // 파일이 업로드 되었다면 저장한다.
    if (file != null && file.getSize() > 0) {      
      try {
        String filename = UUID.randomUUID().toString(); // 임의의 파일명을 준비한다.

        // 파일명의 확장자를 알아낸다.
        int dotIndex = file.getOriginalFilename().lastIndexOf(".");
        if (dotIndex != -1) {
          filename += file.getOriginalFilename().substring(dotIndex);
        }

        // 파일을 지정된 폴더에 저장한다.
        File photoFile = new File("/Users/nana/upload/book/" + filename);
        file.transferTo(photoFile);

        // 저장된 파일명을 도메인 객체에 설정한다.
        book.setPhoto(filename);

      } catch (Exception e) {
        e.printStackTrace();
        return "error!";
      }
    }

    return bookService.add(book);
  }

update도 똑같음. 근데 코드가 중복되니까

  private String saveFile(MultipartFile file) throws Exception {

    String filename = UUID.randomUUID().toString(); // 임의의 파일명을 준비한다.

    // 파일명의 확장자를 알아낸다.
    int dotIndex = file.getOriginalFilename().lastIndexOf(".");
    if (dotIndex != -1) {
      filename += file.getOriginalFilename().substring(dotIndex);
    }

    // 파일을 지정된 폴더에 저장한다.
    File photoFile = new File("/Users/nana/upload/book/" + filename);
    file.transferTo(photoFile);

    return filename;
  }
}

여기서 파일이 유효한지 검사하지 않는다

호출하는 쪽에서 예외가 발생했을 때 어떻게 할 건지

  private String saveFile(MultipartFile file) throws Exception {

    if (file != null && file.getSize() > 0) {

      // 파일을 저장할 때 사용할 파일명을 준비한다.
      String filename = UUID.randomUUID().toString(); // 임의의 파일명을 준비한다.

      // 파일명의 확장자를 알아낸다.
      int dotIndex = file.getOriginalFilename().lastIndexOf(".");
      if (dotIndex != -1) {
        filename += file.getOriginalFilename().substring(dotIndex);
      }

      // 파일을 지정된 폴더에 저장한다.
      File photoFile = new File("/Users/nana/upload/book/" + filename);
      file.transferTo(photoFile);

      return filename;

    } else {
      return null;
    }
  }
}
  @RequestMapping("/book/add")
  public Object add(Book book, MultipartFile file) throws Exception {

    // 파일이 업로드 되었다면 저장한다.
    try {
      book.setPhoto(saveFile(file));
      return bookService.add(book);

    } catch (Exception e) {
      e.printStackTrace();
      return "error!";
    }
    
  }

update도 똑같이 바꿔주기

프론트엔드

1단계 - 입력 화면에 사진 항목을 추가한다.
사진 파일을 업로드할 input 태그 추가

id -> name
서버에 보낼 파라미터 이름

필수입력 항목
값을 입력했는지 검사하기 위해 둔다

photo -> file

multipart로 넘어오는 데이터랑 이름 같으면 안 됨

http://localhost:8080/book/form.html

빈 문자열을 날짜 객체로 못 바꾼다

문자열을 날짜 객체로 바꿔서 저장하는데
빈 문자열은 날짜로 못 바꾼다

javascript formdata 검색

    // 독서일을 지정하지 않았으면 서버에 보내지 않는다.
    if (xReadDate.value == "") {
    	fd.delete("readDate");
    }

독서일을 지정하지 않으면 서버에 보낼 때 readDate를 지워버린다

↑ readDate 없음

썸네일 이미지랑 상세 페이지 들어갔을 때 보이는 이미지랑 다른 파일임
출력 속도가 다름
이미지가 크면 속도가 더 느림
작은 이미지가 빠름

아마추어가 하는 방법
/static/book/index.html

photo/null

스프링 부트의 배치 경로

Spring Boot 실행

Tomcat 서버 실행

OS에서 제공하는 임시 폴더를 사용하여
Application을 배치한다

Windows OS의 경우, 임시 폴더는
C:\Users\사용자홈\AppData\Local\Temp

실행할 때마다 폴더가 달라진다

스프링 부트는 실행할 때마다 위치가 결정된다

docbase

스프링 부트에서 파일 저장 경로

JVM을 실행하는 폴더

C:\Users\사용자홈\MyList\java
↑ JVM 실행 폴더

이클립스 IDE에서 App 클래스를 실행한다면 실행 폴더는 프로젝트 폴더이다.
예) .../mylist-boot/app

이클립스에서 실행하느냐 JVM에서 실행하느냐에 따라

스프링 부트에서 파일 읽기

파일을 저장한 폴더

URL 경로에서 지정한

파일 이름을 파라미터로

spring boot return binary file 검색

https://www.baeldung.com/spring-controller-return-image-file

https://stackoverflow.com/questions/35680932/download-a-file-from-spring-boot-rest-service

진짜 파일명과 오리지널 파일명을 함께 저장
다운로드할 때는 오리지널 파일명을 적어주면 됨

// header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=오리지널파일명");
  @RequestMapping("/book/photo")
  public ResponseEntity<Resource> photo(String filename) {
    try {
      // 다운로드할 파일의 입력 스트림 자원을 준비한다.
      File downloadFile = new File("./upload/book/" + filename); // 다운로드 상대 경로 준비
      FileInputStream fileIn = new FileInputStream(downloadFile.getCanonicalPath()); // 다운로드 파일의 실제 경로를 지정하여 입력 스트림 준비
      InputStreamResource resource = new InputStreamResource(fileIn); // 입력 스트림을 입력 자원으로 포장

      // HTTP 응답 헤더를 준비한다.
      HttpHeaders header = new HttpHeaders();

      // 만약 다운로드 받는 쪽에서 사용할 파일명을 지정하고 싶다면 다음의 응답 헤더를 추가하라!
      // header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=오리지널파일명");

      header.add("Cache-Control", "no-cache, no-store, must-revalidate");
      header.add("Pragma", "no-cache");
      header.add("Expires", "0");

      //      // HTTP 응답 생성기를 사용하여 다운로드 파일의 응답 데이터를 준비한다.
      //      BodyBuilder http응답생성기 = ResponseEntity.ok(); // 요청 처리에 성공했다는 응답 생성기를 준비한다.
      //      http응답생성기.headers(header); // HTTP 응답 헤더를 설정한다.
      //      http응답생성기.contentLength(0); // 응답 콘텐트의 파일 크기를 설정한다.
      //      http응답생성기.contentType(MediaType.APPLICATION_OCTET_STREAM); // 응답 데이터의 MIME 타입을 설정한다.
      //
      //      // 응답 데이터를 포장한다.
      //      ResponseEntity<Resource> 응답데이터 = http응답생성기.body(resource);
      //
      //      return 응답데이터; // 포장한 응답 데이터를 클라이언트로 리턴한다.

      return ResponseEntity.ok()
          .headers(header)
          .contentLength(downloadFile.length())
          .contentType(MediaType.APPLICATION_OCTET_STREAM)
          .body(resource);

    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }

  }

body의 파라미터 타입은 Resource만 가능하기 때문에 Resource로 포장한다

http://localhost:8080/book/photo?filename=9bd261b8-1f71-477b-8d85-d77af3bdb5f5.png

https://developer.mozilla.org/ko/docs/Web/CSS/vertical-align

이미지가 없는 책들도 있다

이렇게 기본 이미지가 뜨게 해야 한다

        if (book.photo == null) {
        	book.photo = "default.png";
        }

자바 썸네일 이미지 만들기 검색

라이브러리 다운 받아야 됨

https://github.com/coobird/thumbnailator

https://search.maven.org/artifact/net.coobird/thumbnailator/0.4.17/jar

// 썸네일 이미지 생성 라이브러리
implementation 'net.coobird:thumbnailator:0.4.17'

gradle eclipse

Refresh

      // 썸네일 이미지 파일 생성
      Thumbnails.of(photoFile)
      	.size(50, 50)
      	.outputFormat("jpg")
      	.toFile(new File("./upload/book/" + "50x50_" + filename));

썸네일 이미지로 만드니까 확실히 파일 크기가 작음

변경했더니 사진 날라감

사진 데이터를 변경하지 않으면 업데이트 항목에서 제외한다.

BookDao.xml
update sql문에 동적

https://mybatis.org/mybatis-3/dynamic-sql.html

  <update id="update" parameterType="book">
    update ml_book set 
      title=#{title}, 
      author=#{author},
      press=#{press},
      feed=#{feed},
      read_date=#{readDate},
      page=#{page},
      price=#{price}
      <if test="photo != null">
        ,photo=#{photo}
      </if>
    where 
      book_no=#{no}
  </update>

sql문을 만들 때 조건문, 반복문을 넣을 수 있다

if
choose (when, otherwise)
trim (where, set)
foreach

Lombok

Lombok 라이브러리를 프로젝트에 추가한다

https://projectlombok.org/

https://projectlombok.org/setup/gradle

https://plugins.gradle.org/plugin/io.freefair.lombok

git/bitcamp-study/mylist-boot/app> gradle compileJava

소스파일을 다 컴파일한다

bin: 이클립스가 컴파일한 파일이 놓여지는 폴더
build: gradle 빌드 도구로 컴파일한 파일이 놓여지는 폴더

javap -cp bin/main com.eomcs.mylist.domain.Member

gradle compileJava

javap -cp build/classes/java/main com.eomcs.mylist.domain.Member

gradle compileJava

javap -cp build/classes/java/main com.eomcs.mylist.domain.Member

이클립스로 컴파일 할 때도 lombok이 적용되도록

https://projectlombok.org/download

승객 목록. 적하 목록.
jar 파일 안에 어떤 것들이 있는지

java -jar lombok.jar

IDE 체크되어 있는지 확인하고 Install/Update 클릭

도메인 클래스에 Lombok을 적용한다

Lombok 생성자

https://projectlombok.org/features/constructor

좋은 웹페이지 즐겨찾기