[Spring Data JPA] 프로필 페이지 유저 사진 변경 - 1
프로필 사진 변경을 구현할것이다.
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를 바꿔주면 끝.