메인화면의 우상단의 프로필을 눌러 프로필 사진을 변경하는 기능을 만들어 보자
프로필 사진을 클릭했을때 연결되는 컨트롤러 설정
매핑된 컨트롤러에서 유효성검사를 한다. (로그인 상태)
@GetMapping("/user/profileUpdateForm")
public String profileUpdateForm(Model model){
User principal = (User) session.getAttribute("principal");
if( principal == null ){
return "redirect:/login"; // 로그인 창으로 이동
}
User userPS = userRepository.findById(principal.getId());
model.addAttribute("user", userPS);
return "user/profileUpdateForm"; // 프로필 사진 변경 창으로 이동
}
프로필사진이 필요하므로 유저 테이블에 profile
을 추가한다
create table user_tb (
id int auto_increment primary key,
username varchar not null unique,
password varchar not null,
email varchar not null,
profile varchar, // 추가
created_at timestamp not null
);
스프링은 staic폴더
를 디폴트로 참조하므로 사진을 추가하거나 수정하면 서버의 static폴더에 사진이 저장되어야 한다.
사진은 물리적 장치에 저장되고 DB는 그 사진의 주소를 저장하고 있을때 클라이언트가 요청한다면 주소만 리턴하면 된다.
기본적으로 더미데이터로 넣어놓은 데이터를 참조하고 있었는데 수정한 프로필 사진을 참조하도록 변경해보자
insert into user_tb (username, password, email, PROFILE, created_at)
values ('ssar','1234','ssar@nate.com', '/images/default_profile.png', now());
insert into user_tb (username, password, email, PROFILE, created_at)
values ('love','1234','love@nate.com', '/images/default_profile.png', now());
수정 페이지를 다음처럼 만들었을 때
파일을 입력받을수 있는 태그를 만든다
<input type="file" class="form-control" id="profile" name="profile" onchange="chooseImage(this)">
chooseImage
는 this
가 ( input 태그 ) 변경되었을때 실행되는 함수를 등록한다.
이미지 파일을 열면 chooseImage()함수
가 실행된다.
function chooseImage(obj)
console.log(obj.files);
를 이용해서 변경된 파일을 확인해보면 배열로 나온다.
console.log(obj.files[0])
를 이용해서 0번째 파일의 메타데이터를 확인할 수 있다.
파일에 이미지를 넣지 않을 경우 경고를 날려준다.
if(!file.type.match("image.*")){
alert("이미지를 등록해야 합니다.")
return;
}
인풋태그를 이용해서 이미지 파일을 넣었으므로 파일을 읽고 프로필의 이미지를 변경시켜야 한다.
파일을 읽기 위해서 다음 객체를 이용한다
let reader = new FileReader();
FileReader
의 reader.readAsDataURL(file)
메소드를 이용해서 파일을 읽는다.
readAsDataURL
는 리턴타입이 void 인데 자바스크립트는 싱글스레드 이기 때문에 바로 리턴을 받지 않는다.
내부적으로 비동기가 구현되어 있기 때문에 다른일을 하다가 파일을 읽는 I/O 가 완료되면 콜백을 듣고 돌아와 다시 작업을 진행한다.
I/O 가 끝나면 FileReader
의 onload
프로퍼티를 이용해서 이후 작업을 진행해주면 된다.
이후 작업은 사용자가 올린 이미지파일을 읽어서 프로필 사진안에 넣는다
reader.onload = function (e){
// 콜백함수를 등록 readAsDataURL 끝나면 다음 함수를 실행해라 !
// console.log(e);
$('#imagePreview').attr("src",e.target.result);
}
console.log(e);
를 이용해서 발생한 이벤트의 정보를 확인해보면
currentTarget
이나 target
에 파일의 데이터가 들어있는것을 확인할 수 있다.
이미지 파일을 올려보면
chooseImage()
-> FileReader
의readAsDataURL()
-> onload
->$('#imagePreview').attr("src",e.target.result)
가 실행된다. 인코딩 타입 변경
파일을 폼태그로 보내기 위해서는 인코딩 타입의 변경이 필요하다
디폴트는 enctype="application/x-www-form-urlencoded"
인데
파일을 보내기 위해서는 아래처럼 변경시켜야 한다.
<form action="/user/profileUpdate" method="post" enctype="multipart/form-data">
multipart/form-data
에서 multipart
은 여러가지의 타입을 조합하게 만들어주고 form-data
는 폼에 모든타입을 동시에 전송하게 해준다.
사진을 보낼때 폼태그를 사용하지 않고 ajax로 보낸다면 버튼에 함수를 등록한다
<button type="button" class="btn btn-primary" onclick="updateImage()">사진 변경</button>
이번에는
formData
를 이용해서 이미지를 보내보자
function updateImage() {
let profileForm = $('#profileForm')[0]; // 이녀석도 배열로 리턴이 된다.
// console.log(profileForm);
let formData = new FormData(profileForm); // 폼의 모든 데이터를 가지고 있다.
$.ajax({
type: "put",
url: "/user/profileUpdate",
contentType: false, // x-www 으로 파싱하지 마라
processData: false, // 쿼리스트링으로 파싱하지 마라
data: formData,
enctype: "multipart/form-data",
dataType: "json"
}).done((res) => {
alert(res.msg);
location.href = "/"
}).fail((err) => {
alert(err.responseJSON.msg);
});
}
<form id="profileForm">
의 데이터를 확인하면 -> console.log(profileForm);
이미지 데이터가 들어있는걸 확인할 수 있다.
이번에 보내는 타입은 json
이 아닌 formData
로 변경하고 인코딩타입을 "multipart/form-data"
타입으로 보내야한다.
여기서 주의할점은 보내는 타입이 formData
라면 기본적으로 x-www~
타입으로 보내려고 하는데 contentType: false
을 이용해서 기능을 꺼줘야 한다
contentType 이 false
가 되면 자동적으로 쿼리스트링으로 파싱하려고 하는데 이것도 processData: false
으로 해제시켜야 한다.
formData를 이용해서 멀티파트 데이터를 전송하려고 하면 다음 4가지는 세트처럼 지켜야한다.
contentType: false,
processData: false,
data: formData,
enctype: "multipart/form-data"
@PostMapping("/user/profileUpdate")
public String profileUpdate(MultipartFile profile) {
User principal = (User) session.getAttribute("principal");
if( principal == null ){
return "redirect:/login";
}
if( profile.isEmpty()){
throw new CustomException("사진이 전송 되지 않았습니다.");
}
// 사진이 아닐 경우 exception 처리 필요..
User userPS = service.프로필사진수정(profile, principal.getId());
session.setAttribute("principal", userPS);
return "redirect:/";
}
폼태그에서 multipart/form-data
로 보낸 데이터를 받기 위해서 MultipartFile
타입을 이용한다.
간단한 유효성 검사를 한 뒤에 서비스를 통해서 사진수정을 해보자
사진수정이 완료되면 세션을 새롭게 넣어서 변경된 프로필을 사용자게 보여주면 된다.
@PutMapping("/user/profileUpdate")
public ResponseEntity<?> profileUpdate(MultipartFile profile) throws Exception{
User principal = (User) session.getAttribute("principal");
if( principal == null ){
throw new CustomApiException("로그인이 필요한 페이지 입니다.", HttpStatus.UNAUTHORIZED);
}
System.out.println(profile.getContentType());
if( profile.isEmpty()){
throw new CustomApiException("사진이 전송 되지 않았습니다.");
}
User userPS = service.프로필사진수정(profile, principal.getId());
session.setAttribute("principal", userPS);
return new ResponseEntity<>(new ResponseDto<>(1, "수정 성공", null), HttpStatus.OK);
}
폼태그와 유사하지만 익셉션은 customApiException으로 발생시킨다.
@Transactional
public User 프로필사진수정(MultipartFile profile, int pricipalId){
// 1번 사진을 /static/image에 UUID로 변경해서 저장
String uuidImageName = PathUtil.writeImageFile(profile);
// 2번 저장된 파일의 경로를 DB에 저장
User userPS = userRepository.findById(pricipalId);
userPS.setProfile(uuidImageName);
userRepository.updateById(userPS.getId(), userPS.getUsername(), userPS.getPassword(), userPS.getEmail(), userPS.getProfile(), userPS.getCreatedAt());
return userPS;
}
서비스에서는 먼저 사용자가 수정 요청한 사진을 물리장치에 저장해야 한다.
이후 저장된 경로를 DB 의 유저테이블에 update를 해야한다.
여러사용자가 같은 이름의 이미지 파일을 업로드 할 수 있으므로 UUID
를 이용해서 파일이름을 살짝 변형한뒤 저장한다.
잠시 설명하자면
UUID
는 네트워크상에 고유성이 보장되는 id를 만들기위한 규약인데 해시
를 이용해서 다양한 길이를 가진 데이터를 일정한 길이의 데이터로 암호화한 것이다.
해시를 이용하면 알고리즘을 통해서 일정한 길이의 데이터로 변환이 되는데 암호화만 가능하고 복호화는 불가능한 특징을 가지고 있다.
데이터가 완전 동일한 해시는 같은 결과가 나오고 1bit 라도 데이터에 변화가 생긴다면 전혀 다른 해시값이 나온다.
이를 이용해서 원본데이터에서 변형이 되었는지를 확인하는데 사용되는데 약간이라도 변형이 일어나면 신뢰성이 없는 데이터가 되고 해시값이 다르므로 전혀 다른 데이터가 된다.
이를 이용해서 이미지 데이터에도 해시를 적용한 UUID를 붙여서 이름이 같아도 UUID값이 다른 고유한 이미지파일이름을 만들어서 저장한다.
UUID로 변경해서 저장하고 DB에 저장하는 과정은 로직은 반복될 예정이므로 로직을 분리하자.
유틸클래스로 분리
public class PathUtil {
private static String getStaticFolder(){
return System.getProperty("user.dir") + "\\src\\main\\resources\\static\\";
}
public static String writeImageFile(MultipartFile profile){
UUID uuid = UUID.randomUUID();
String uuidImageDBName = "/images/"+uuid+"_"+profile.getOriginalFilename();
String uuidImageRealName = "\\images\\"+uuid+"_"+profile.getOriginalFilename();
String staticFolder = getStaticFolder();
Path imageFilePath = Paths.get(staticFolder+"\\"+uuidImageRealName);
try {
Files.write(imageFilePath, profile.getBytes()); // 내부적으로 비동기. .. 스레드가 있음
}catch (Exception e){
throw new CustomException("사진을 웹서버에 저장하지 못하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
}
return uuidImageDBName;
}
}
getProperty()
메소드는 경로를 붙여주는 역할을 한다.
writeImageFile()
에 멀티파트 데이터를 입력하면 UUID
를 붙인 DB에 저장할 uuidImageDBName
를 하나 만든다.
getProperty()
와 uuidImageRealName
을 이용해서 물리장치의 주소를 Files.write()
에게 넘겨준다
Files.write()
는 물리장치에 파일을 저장하게 되는데 내부적으로 스레드가 있기때문에 다른작업이 가능하다.
저장이 완료되면 DB에 주소를 저장하기 위해 uuidImageDBName
를 리턴한다.
리턴된 주소는 DB에 저장하게 된다.
userRepository.updateById(userPS.getId(),
userPS.getUsername(),
userPS.getPassword(),
userPS.getEmail(),
userPS.getProfile(),
userPS.getCreatedAt());
사용된 쿼리는
<update id="updateById">
update user_tb set username =#{username}, password=#{password},
email=#{email}, profile = #{profile} where id = #{id}
</update>
사진을 저장할경우 DB의 변화는
변경완료