Spring/JPA + Security

[Spring Data JPA] 프로필 페이지 유저 사진 변경 - 1

pyogowoon 2023. 1. 14. 13:42

 

프로필 사진 변경을 구현할것이다.

 

3가지의 목표가 있다.

 

 1. pageUserId 와 principalId 를 비교해서 둘이 동일할때만 동작하기.

 2. 사진을 클릭시 input type="file" 강제로 클릭 이벤트 발생시키기.

 3. 이미지를 put방식 (ajax)을 사용하여 서버로 전송시켜보기 - formData 객체 이용

 

profile.jsp

<!--유저이미지-->
<div class="profile-left">
   <div class="profile-img-wrap story-border"
      onclick="popup('.modal-image')">

      <!-- 프로필 사진띄우기 form태그 -->

      <form id="userProfileImageForm">
         <input type="file" name="profileImageFile" style="display: none;"
            id="userProfileImageInput" />
      </form>

              <!-- 프로필 사진띄우기 form태그 end-->
           
      <img class="profile-image" src="#"
         onerror="this.src='/images/person.jpeg'" id="userProfileImage" />
   </div>
</div>
<!--유저이미지end-->

현재 이렇게 되어있다.

 div class 안에 form 태그와 img 태그가 있고 둘중 아무거나 클릭해도

 onclick="popup('.modal-image')" 에 의해 온클릭 이벤트가 발생한다.

 그렇다면 modal-image 클래스 를 찾아가보자.

 

 

<div class="modal-image" onclick="modalImage()">
   <div class="modal">
      <p>프로필 사진 바꾸기</p>
      <button onclick="profileImageUpload()">사진 업로드</button>
      <button onclick="closePopup('.modal-image')">취소</button>
   </div>
</div>

 이렇게 되어있다.

 

 

 

사진속 고양이 있는곳을 클릭하면 뜬다.

 

function profileImageUpload() {
   $("#userProfileImageInput").click();

   $("#userProfileImageInput").on("change", (e) => {
      let f = e.target.files[0];

      if (!f.type.match("image.*")) {
         alert("이미지를 등록해야 합니다.");
         return;
      }

      // 사진 전송 성공시 이미지 변경
      let reader = new FileReader();
      reader.onload = (e) => {
         $("#userProfileImage").attr("src", e.target.result);
      }
      reader.readAsDataURL(f); // 이 코드 실행시 reader.onload 실행됨.
   });
}

즉 사진 업로드를 클릭하면 강제로 input type ="file" 이 발생된다.

 그럼 이제 DB에 반영해서 사진을 받아오면 된다. input type을 이용해서 put 할것이다.

 

 

 

 


<!--프로필사진 바꾸기 모달-->
<div class="modal-image" onclick="modalImage()">
   <div class="modal">
      <p>프로필 사진 바꾸기</p>
      <button onclick="profileImageUpload(${dto.user.id},${principal.user.id})">사진 업로드</button>
      <button onclick="closePopup('.modal-image')">취소</button>
   </div>
</div>

profile.jsp 에서 사진 업로드를 하기 위해 onclick="profileImageUpload() 매개변수에 dto.user.id 와 principalId를 같이 보낸다.

 여기서 principal  은 시큐리티 태그라이브러리로 받아왔던 것 ( 이전에 header에서 받은것)이다.

 

 

 

// (3) 유저 프로파일 사진 변경 (완)
function profileImageUpload(pageUserId , principalId) {
        //데이터 잘 와지는지 테스트
        console.log("pageUserId" , pageUserId);
        console.log("principalId" , principalId);

        if(pageUserId != principalId){
            alert("아이디의 주인만 사진을 변경할 수 있습니다.");
            return;
        }

   $("#userProfileImageInput").click();

   $("#userProfileImageInput").on("change", (e) => {
      let f = e.target.files[0];

      if (!f.type.match("image.*")) {
         alert("이미지를 등록해야 합니다.");
         return;
      }

페이지의 주인과 아이디의 주인이 일치할 경우에만 프로필 사진을 바꿀 수 있도록 하기 위해

if문으로 비교하는 과정을 가지고,  아래의 코드는 파일 선택 시 이미지 바뀌는 로직.

 

그렇다면 이제 ajax로 데이터를 보내야 하고, 보낼 때 formData를 보내야 하기 때문에 formData를 구하는 과정을 거쳐야 한다.

 

$("#userProfileImageInput").on("change", (e) => {
   let f = e.target.files[0];

   if (!f.type.match("image.*")) {
      alert("이미지를 등록해야 합니다.");
      return;
   }

   let profileImageForm = $("#userProfileImageForm");
   console.log(profileImageForm);

 

****  데이터가 잘 넘어와지는지 찍어봤는데

 

$("userProfileImageForm")[0]; 처럼 0번지를 찾아야 잘 찾아진다 (그냥 참고만)

어쨌든

 

 

 

 

 

    //FormData 객체를 이용하면 form 태그의 필드와 그 값을 나타내는 일련의
      //key/value 쌍을 담을 수 있다.
      let formData = new FormData(profileImageForm);
//    console.log("폼 데이타" , formData);

      $.ajax({
          type: "put",
          url: `/api/user/${principalId}/profileImageUrl`,
          data: formData,
          contentType: false, //필수 이유 : 디폴트값이 www-x-urlencorded로 파싱되는것 방지
            processData:false,   //필수  이유: contentType을 false로 줬을 떄 QueryString 자동설정됨. 해제
            encType:"multipart/form-data",
            dataType:"json"
      }).done(res => {

            // 사진 전송 성공시 이미지 변경
          let reader = new FileReader();
      reader.onload = (e) => {
         $("#userProfileImage").attr("src", e.target.result);
          }
          reader.readAsDataURL(f); // 이 코드 실행시 reader.onload 실행됨.

      }).fail(error => {
           console.log(error,"오류");
      });




   });
}

 위의 ajax 방식은 온전히 사진만 업데이트(Put) 할때 사용하는 방식이다.

 

보통은 AJAX로 폼(form 태그) 전송을 할 일이 거의 없다. 주로 JSON 구조로 키-값 데이터를 전송하기 때문인데. 하지만 폼 전송이 필요한 경우가 있다. 바로 이미지를 AJAX로 업로드할 경우다.

 

FormData 객체 사용

FormData 객체를 이용하면 Form 태그의 필드와 그 값을 나타내는 일련의 key/value 값의 쌍을 담을 수 있다.

 즉 값만 담긴다고 보면 된다.

 

이번엔 FormData 방식을 사용했는데,

 

이전의 update.js에서 보면 Data를 Form태그를 들고와서 .serialize 해서 받아왔었는데

form.serialize 하는 경우는 form태그의 요소가 많을때 사용한다. 

여기서 주의할 점은 serialize는 type=file 요소는 포함하지 않는다는 것이다.

 즉 사진을 전송하기 위해선 FormData를 써야 하는것이다.

 

 그렇기떄문에 사진 데이터만 받아왔기 떄문에 FormData 사용하였고

사진만 전송하는 방식에선 꼭

 

 

contentType 

false 를 해줘야한다. default 값이 x-www-form-urlencoded 때문에(가장 자주쓰는 그거)

 urlencoded 로 파싱되는것을 방지하기 위해

 

 

 processData

false 로 해줘야한다. 이유는 contentType 을 false로 줬을 때 QueryString 이 자동으로 설정되는데 이를 

해제하기 위해서.

 

encType

multipart/form-data 로 설정해준다.

 

 

그다음 UserApiController로 넘어가

 

@PutMapping("/api/user/{principalId}/profileImageUrl")
public ResponseEntity<?> profileImageUrlUpdate(@PathVariable int principalId, MultipartFile profileImageFile,
   @AuthenticationPrincipal PrincipalDetails principalDetails){

   User userEntity =  userService.회원프로필사진변경(principalId , profileImageFile);
   principalDetails.setUser(userEntity); // 세션 변경

    return new ResponseEntity<>(new CMRespDto<>(1,"이미지 저장성공",null), HttpStatus.OK);
}

 

 

업데이트 할것이기 때문에 PutMapping을 사용하였고 추가적으로 세션을 변경해줘야 하기 떄문에 @AuthenticationPrincipal 사용

 

이전 이미지 업로드할땐 ajax를 사용하지 않았는데 그땐 여러가지 받아야해서 Dto사용해서 받음,

그땐 Dto에서 따로 private MultipartFile file; 로 따로 받아줬었는데

 이렇게 받을때는 매개변수에 MultipartFile (input type에서 name 이름) 으로 정확히 받아줘야한다.

 

 

 

UserService로 넘어가서

 @Value("${file.path}") // yml에 적힌값 가져오는거
    private String uploadFolder;


    @Transactional
    public User 회원프로필사진변경(int principalId, MultipartFile profileImageFile){
        // 프로필 업로드랑 로직 동일함
        UUID uuid = UUID.randomUUID(); // uuid
        String imageFileName = uuid+"_"+ profileImageFile.getOriginalFilename();

        Path imageFilePath = Paths.get(uploadFolder+imageFileName);

        try{
            Files.write(imageFilePath, profileImageFile.getBytes());
        }catch(Exception e){
            e.printStackTrace();

        }

            User userEntity = userRepository.findById(principalId).orElseThrow(()->{
                return new CustomApiException("유저를 찾을 수 없습니다.");
            });

        userEntity.setProfileImageUrl(imageFileName);

        return userEntity;
        //더티체킹으로 업데이트 됨.

    }
}

 

 파일의 경로를 알아야하기 떄문에 @Value 사용해주고

프로필 업로드와 동일한 로직을 해준다.

 

<!-- 프로필 사진띄우기 form태그 end-->

<img class="profile-image" src="/upload/${dto.user.profileImageUrl}"
   onerror="this.src='/images/person.jpeg'" id="userProfileImage" />

 

 

img src를 바꿔주면 끝.