끄적끄적 코딩일지

[Spring]이미지 월드컵 만들기 (6)- 이미지 업로드(2) 본문

Project/[Spring] 이미지 월드컵 만들기

[Spring]이미지 월드컵 만들기 (6)- 이미지 업로드(2)

BaekGyuHyeon 2022. 5. 24. 19:04

저번글에서 대충 선택한 이미지 만큼 동적으로 해당 이미지와 이미지의 타이틀을 입력하는 항목을 추가하는것까지는 하였다.

2022.05.23 - [Project/[Spring] 이미지 월드컵 만들기] - [Spring]이미지 월드컵 만들기(5) - 이미지 업로드 설계(1)

 

[Spring]이미지 월드컵 만들기(5) - 이미지 업로드 설계(1)

원래 저번글에서 마치고 Spring Security를 하려고 했으나 괜히 아직 없는 기능에 보안부터 적용하는것 같아서 그냥 UI부터 설계하기로 했다. 만들어야할 페이지는 메인(월드컵 리스트 표시),로그인,

blablacoding.tistory.com

그렇게 만들었으니 서버에 올라가는걸 테스트 할 차례

일단 그대로 서버에서 어떻게 값이 날라올지 확인해 보았다.

 

@PostMapping(value = "/world-cup-game")
    public WorldCupGame makeWorldCupGame(WorldCupGame game){
        System.out.println(game.toString());
        return game;
    }

결과는..

{name:null,createTime:null,updateTime:null,id:0,makeMember:null,description:ttt,games:[]}

?????

 

desription 외에 아무것도 변한게 없다.

 

참고로 createTime,updateTime,description은 Entity에 추가한것

createTime과 updateTime 은 따로 클레스를 만들어서 extends 하도록 했다.

더보기
package com.example.springTest.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class TimeStamp {
    @CreatedDate
    private LocalDateTime createTime;

    @LastModifiedDate
    private LocalDateTime updateTime;
}

 

뭐 이미지 파일은 한번에 성공할꺼라고는 생각 안하고 있었고.... 그렇다고 해도 값 WorldCupGame의 title도 전달이 안되다니???

 

확인결과 이부분도 실수, WorldCupGame에 변수명을 name으로 해놓고서는 <input name="title">으로 해놓았던것.

 

이건 수정하고 페이지 검사를 통해 왜 값이 넘어가지 않았는지 페이지를 검사해보았다.

 

 

아....

동적으로 추가한 tag의 name값 수정이 않되었다.

예상되는 원인은 Javascript코드중 children(".img-title")을 못찾은것.

jquery는 편하긴 한데 가끔씩 아무런 오류도 내뱉지 않을때가 있다.

 

암튼 children(".card-img-top")은 잘 인식해서 src값을 변경한것을 보니까

div 하위 노드는 찾지 못한 모양.

보나마나 children 한번만 사용해서는 하위 노드의 하위노드를 찾지 못하는 것이겠지....

 

selectImageFile=(event)=>{
    $('#img-list').empty();
    let fileInput = $('#file-select')[0].files;
    for(let i = 0; i < fileInput.length;i++){
        let clone = $('#original').clone();
        clone.children(".input-img").attr("src",URL.createObjectURL(fileInput[i]));
        clone.children(".card-body").children(".img-file").files = fileInput[i];
        clone.children(".card-body").children(".img-file").attr("name","games["+i+"].image");
        clone.children(".card-body").children(".img-title").attr("name","games["+i+"].title");
        clone.css("display","");
        $('#img-list').append(clone);
    }
}

그래서 children을 두번써서 실행 결과는....?

 

오류가 났다.

 

오류 내용을 보아하니 WorldCupItem의 image는 byte[] 인데 Multipartfile을 변환하지 못한다는 의미...

이걸 어떻게 할까 구글링을 열씸히 한 결과....

 

 

 

 

 

해결 방법 못찾음.... 키워드를 잘못 한걸지도 몰라도 죄다 한개의 input에서 여러개의 파일을 선택하는 multiple만 나온다.

또한 Multipart 자체를 byte[]를 포함한 Entity로 메핑하는 방법도 찾지를 못하였다.

그렇다고 form tag를 포기하고 ajax나 axios으로 구현하자니 작업이 많아져서 귀찮아진다.

 

하지만 개발자로써

확실히 구현할 수 있지만 오래걸리고 많은 오류를 거처야 하는 방법과 구현이 가능할지 모르지만 성공만 하면 간단하게 구현할 수 있는 길중 선택하라면 무조건 간단한 방법이라고 생각한다.

더보기

TMI : 게으른 개발자가 열심히 하는 개발자 보다 더 낳다라는 말이 있다. 열심히 하는 개발자가 코드 하나하나 다 칠때 게으른 개발자는 반복작업이 귀찮아서 모듈을 많들어 내고 수정을 적게 할수있는 구조로 개발을 한다. 때문에 기존 방식에 불평불만을 내놓는 게으른 개발자가 혁신을 일으킬 수 있다. 

 

그래서 고민의 고민을 한 결과

그냥 한개의 input에서 다중 파일을 업로드하는 방식으로 구현하고 서버쪽에서 이를 합치기로 했다.

 

@RestController
public class GameController {
    @Autowired
    WorldCupGameService gameService;
    @PostMapping(value = "/world-cup-game", produces = MediaType.IMAGE_JPEG_VALUE)
    public byte[] makeWorldCupGame(WorldCupGame game,@RequestPart List<MultipartFile> files){
        game.build(files);
        return game.getGames().get(0).getImage();
    }
}

Controller에서는 이미지 byte값이 제대로 들어갔는지 확인하기 위해 잠시 이미지를 return하는것으로 수정하였다.

 

아래는 build method 추가

package com.example.springTest.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;

import javax.persistence.*;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@Entity
@ToString
public class WorldCupGame extends TimeStamp {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private long id;
    @Column
    private String name;
    @ManyToOne(fetch = FetchType.LAZY,targetEntity = Member.class,cascade = CascadeType.ALL)
    @JoinColumn
    private Member makeMember;
    @Column
    private int playCount = 0;
    @Column
    private String description;
    @OneToMany(fetch = FetchType.LAZY,targetEntity=WorldCupItem.class,cascade = CascadeType.ALL)
    private List<WorldCupItem> games = new ArrayList<WorldCupItem>();

    public void addItem(WorldCupItem item){
        this.games.add(item);
    }
    public void removeItem(WorldCupItem item){
        this.games.remove(item);
    }
    public void build(List<MultipartFile> files){
        for (int i = 0; i < files.size(); i++) {
            try {
                games.get(i).setImage(files.get(i).getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="//unpkg.com/bootstrap@4/dist/css/bootstrap.min.css">
    <script src='//unpkg.com/jquery@3/dist/jquery.min.js'></script>
    <script src='//unpkg.com/bootstrap@4/dist/js/bootstrap.min.js'></script>
    <script type="text/javascript" src="/js/worldcup_add.js"></script>
</head>
<body>

  <form id="worldcup-form" class="p-3" action="/world-cup-game" method="post" enctype="multipart/form-data">
      <label for="worldcup-title" class="form-label">월드컵 Title</label>
      <input id="worldcup-title"  name="name" class="form-control">
      <label for="worldcup-dcs" class="form-label">월드컵 설명</label>
      <input id="worldcup-dcs" class="form-control" name="description">
      <input id="file-select" name="files" type="file" class="btn btn-outline-primary" value="이미지 파일 선택" accept=".png,.jpg" onchange="selectImageFile()" multiple>
      <div class="container">
          <div class="card col-md-3" id="original" style="display: none">
              <img class="input-img card-img-top" src="">
              <div class="card-body">  <!-- <input type="file"> 삭제! -->
              <input class="form-control img-title" type="text" placeholder="이미지 설명..." name="">
              </div>
          </div>
          <div class="row" id="img-list">

          </div>
          <input type="submit" class="btn btn-outline-success" value="월드컵 만들기">
      </div>
  </form>
</body>
</html>
selectImageFile=(event)=>{
    $('#img-list').empty();
    let fileInput = $('#file-select')[0].files;
    for(let i = 0; i < fileInput.length;i++){
        let clone = $('#original').clone();
        clone.children(".input-img").attr("src",URL.createObjectURL(fileInput[i]));
        // clone.children(".card-body").children(".img-file").files = fileInput[i];
        // clone.children(".card-body").children(".img-file").attr("name","files["+i+"].file");
        clone.children(".card-body").children(".img-title").attr("name","games["+i+"].title");
        clone.css("display","");
        $('#img-list').append(clone);
    }
}

이전에 이미지 개수만큼 <input type="file">을 추가해서

 clone.children(".card-body").children(".img-file").files = fileInput[i]; 으로 하나하나 files 값을 세팅하는것이 아니라

이미지 선택하는 input에서 모든 값을 List<MultipartFile>으로 보내고 그것을 WorldCupGame에 셋팅하는 방식으로 바꿨다.

일종의 편법인 샘

 

과연 그 결과는???

 

 

 

엌ㅋㅋㅋㅋㅋㅋㅋ 이게 진짜로 되네 ㅋㅋㅋㅋㅋㅋㅋㅋ

이 편법이 진짜로 먹힐줄이야 ㅋㅋㅋㅋㅋㅋ

 

System.out.println찍어서 title과 이미지가 제대로 mapping이 되는지도 확인하였다.

 

물론 제대로된 방법은 아니라서 나중에 다시 알아봐야 하겠지만 말이다.

 

다시 코드를 수정해서 Database에 잘 수정이 되는지 확인해보자

오... 아주 잘 들어가는데?

그럼 여기까지 한 김에 Database에서 이미지를 조회해서 리턴하는 Api를 만들어 보자

 

@RestController
public class GameItemAPI {
    @Autowired
    private WorldCupGameItemService service;
    @GetMapping(value = "/image/{id}",produces = MediaType.IMAGE_JPEG_VALUE)
    public byte[] getImage(@PathVariable("id") String gameItemId){
        System.out.println(gameItemId);
        return service.getImageById(Long.parseLong(gameItemId));
    }
}


@Service
@Getter
public class WorldCupGameItemService {
    @Autowired
    private WorldCupItemRepo repo;
    public byte[] getImageById(long id){
        return repo.findById(id).get().getImage();
    }
}

 

 

아주 만족스럽게 잘 나온다

 

데이터 생성 기능은 얼추 되었으니

다음글에서는 메인페이지 및 게임 페이지를 만들어 보겠다.