html
<main>
<!-- 헤더 추가 -->
<th:block th:replace="~{common/header}"></th:block>
<section class="myPage-content">
<!-- 사이드 메뉴(왼쪽) 추가 -->
<th:block th:replace="~{myPage/sideMenu}"></th:block>
<!-- 마이페이지 본문(오른쪽) -->
<section class="myPage-main">
<h1 class="myPage-title">프로필</h1>
<span class="myPage-subject">프로필 이미지를 변경할 수 있습니다.</span>
<form action="profile" method="POST" name="myPageFrm" id="profile" enctype="multipart/form-data">
<div class="profile-image-area">
<img th:with="user=#{user.default.image}"
th:src="${session.loginMember.profileImg ?: user}"
id="profileImg">
</div>
<span id="deleteImage">x</span>
<div class="profile-btn-area">
<label for="imageInput">이미지 선택</label>
<input type="file" name="profileImg" id="imageInput" accept="image/*">
<button>변경하기</button>
</div>
<div class="myPage-row">
<label>이메일</label>
<span th:text="${session.loginMember.memberEmail}"></span>
</div>
<div class="myPage-row">
<label>가입일</label>
<span th:text="${session.loginMember.enrollDate}"></span>
</div>
</form>
</section>
</section>
</main>
<script th:inline="javascript">
const loginMemberProfileImg = /*[[${session.loginMember.profileImg}]]*/ "회원 프로필 이미지";
</script>
js
/* 프로필 이미지 추가/변경/삭제 */
// 프로필 이미지 페이지 form 태그
const profile = document.querySelector("#profile");
// 프로필 이미지가 새로 업로드 되거나 삭제 되었음을 기록하는
// 상태 변수
// -1 : 초기 상태(변화 없음)
// 0 : 프로필 이미지 삭제
// 1 : 새 이미지 선택
let statusCheck = -1;
// input type="file" 태그의 값이 변경 되었을 때
// 변경된 상태를 백업해서 저장할 변수
// -> 파일이 선택/취소된 input을 복제해서 저장
// 요소. cloneNode(true|false) : 요소 복제(true 작성 시 하위 요소도 복제)
let backupInput;
// profile form태그가 화면에 있다면
if(profile != null){
// 1) 프로필 이미지 수정에 사용할 요소 모두 얻어오기
// img 태그 (프로필 이미지가 보여지는 요소)
const profileImg = document.querySelector("#profileImg");
// input type="file" 태그 (실제 업로드할 프로필 이미지를 선택하는 요소)
let imageInput = document.querySelector("#imageInput");
// x버튼 (프로필 이미지를 제거하고 기본 이미지로 변경하는 요소)
const deleteImage = document.querySelector("#deleteImage");
// 3) changeImageFn 함수 정의하기
/* input type="file"의 값이 변했을 때 동작할 함수(이벤트 핸들러) */
const changeImageFn = e => {
// 업로드 가능한 파일 최대 크기 지정하여 필터링
const maxSize = 1024 * 1024 * 5;
// 5MB == 1024KB * 5 == 1024B * 1024 * 5
console.log("e.target", e.target); // input 태그
console.log("e.target.value", e.target.value); // 변경된 값(파일명)
// 선택된 파일에 대한 정보가 담긴 배열 반환
// -> 왜 배열?? multiple 옵션에 대한 대비(파일 여러개 받을 때)
console.log("e.target.files", e.target.files);
// 업로드된 파일이 1개 있으면 files[0]에 저장됨
// 업로드된 파일이 없으면 files[0] == undefined
console.log("e.target.files[0]", e.target.files[0]);
const file = e.target.files[0];
// ------------ 업로드된 파일이 없다면(취소한 경우)------------
if(file == undefined) {
console.log("파일 선택 후 취소됨");
// 파일 선택 후 취소 -> value == ''
// -> 선택한 파일 없음으로 기록됨
// -> backupInput으로 교체 시켜서
// 이전 이미지가 남아 있는 것 처럼 보이게 함
// 백업의 백업본
const temp = backupInput.cloneNode(true);
console.log("temp", temp); // 백업용 input태그
// input 요소 다음에 백업 요소 추가
imageInput.after(backupInput);
// 화면에 존재하는 기존 input 제거
imageInput.remove();
// imageInput 변수에 백업을 대입해서 대신하도록 함
imageInput = backupInput;
// 화면에 추가된 백업본에는
// 이벤트 리스너가 존재하지 않기 때문에 추가
imageInput.addEventListener("change", changeImageFn);
// 한번 화면에 추가된 요소(backupInput)는 재사용 불가능
// backupInput의 백업본이 temp를 backupInput 으로 변경
backupInput = temp;
return; // 다른 코드 수행할필요없이 바로 return
}
// ----------- 선택된 파일이 최대 크기를 초과한 경우 ------------
if(file.size > maxSize){
alert("5MB 이하의 이미지 파일을 선택해 주세요.");
//파일을 선택할 때 5MB보다 큰 파일을 선택하면
//일단 무조건 선택은 됨.
//근데 우리는 5MB보다 큰 파일은 취급 안하고 싶음
//그래서 대입된 5MB 초과한 파일을 없애버리겠다
// 아직 변경된적없는 초기상태에서 5MB 초과하는 이미지를 선택한 경우
if(statusCheck == -1){
imageInput.value = '';
} else { // 기존에 변경하려고 선택한 이미지가 있는데
// 다음에 선택한 이미지가 최대 크기를 초과한 경우
// -> 비워버리면 안되고, 그 전에 선택한 이미지로 계속 보이게끔 처리해야함.
// 백업의 백업본
const temp = backupInput.cloneNode(true);
// input 요소 다음에 백업 요소 추가
imageInput.after(backupInput);
// 화면에 존재하는 기존 input 제거
imageInput.remove();
// imageInput 변수에 백업을 대입해서 대신하도록 함
imageInput = backupInput;
// 화면에 추가된 백업본에는
// 이벤트 리스너가 존재하지 않기 때문에 추가
imageInput.addEventListener("change", changeImageFn);
// 한번 화면에 추가된 요소(backupInput)는 재사용 불가능
// backupInput의 백업본이 temp를 backupInput 으로 변경
backupInput = temp;
}
return; // 다른 코드 수행할필요없이 바로 return
}
// ------------- 선택된 이미지 미리보기 ----------------
// JS에서 파일을 읽을 때 사용하는 객체
// - 파일을 읽고 클라이언트 컴퓨터에 저장할 수 있음
/*FileReader 객체는 웹 애플리케이션에서 비동기적으로 파일의 내용을 읽을 수 있게 해줍니다. */
const reader = new FileReader();
// 선택한 파일(file) 을 읽어와
// BASE64 인코딩 형태로 읽어와 result 변수에 저장
reader.readAsDataURL(file); // -> 읽어오기 이벤트(load)
// readAsDataURL() : 파일을 BASE64 형식의 데이터 URL로 읽어들입니다.
// console.log("reader:",reader);
// result에 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW" 이런식으로 들어감
// 읽어오기 끝났을 때 (파일 읽기 작업이 완료되면 이벤트 핸들러 함수를 실행)
reader.addEventListener("load", e => {
// e.target == reader
// 읽어온 이미지 파일이 BASE64 형태로 반환됨
const url = e.target.result; // reader.result
// 프로필 이미지(img)에 src속성으로 url값 세팅
profileImg.setAttribute("src", url);
// 새 이미지 선택 상태를 기록
statusCheck = 1;
// 파일이 선택된 input을 복제해서 백업
backupInput = imageInput.cloneNode(true);
});
};
// 2) imageInput에 change 이벤트로 changeImageFn 등록
// change 이벤트 : 새로운 값이 기존 값과 다를 경우 발생
imageInput.addEventListener("change", changeImageFn);
// ------------ 4) x버튼 클릭 시 기본 이미지로 변경 ----------------
deleteImage.addEventListener("click", () => {
// 프로필 이미지(img)를 기본 이미지로 변경
profileImg.src = "/images/user.png";
// input에 저장된 값(value)를 ''(빈칸)으로 변경
// -> input에 저장된 파일 정보가 모두 사라짐 == 데이터 삭제
imageInput.value = '';
backupInput = undefined; // 백업본도 삭제
// 삭제 상태임을 기록
statusCheck = 0;
});
// ------------ 5) #profile (form) 제출 시 -----------------
profile.addEventListener("submit", e => {
let flag = true;
// loginMemberProfileImg : myPage-profile.html 하단에 script를 이용하여 타임리프로 선언해둔 변수
// submit 해도 되는 경우 :
// 1. 기존 프로필 이미지가 없다가 새 이미지가 선택된 경우
if(loginMemberProfileImg == null && statusCheck == 1) flag = false;
// 2. 기존 프로필 이미지가 있다가 삭제한 경우
if(loginMemberProfileImg != null && statusCheck == 0) flag = false;
// 3. 기존 프로필 이미지가 있다가 새 이미지가 선택된 경우
if(loginMemberProfileImg != null && statusCheck == 1) flag = false;
// 나머지의 경우는 기존 상태에서 변경사항이 없는 경우임.-> 제출막기
if(flag){ // flag 값이 true인 경우
e.preventDefault();
alert("이미지 변경 후 클릭하세요")
}
});
}
/* [input type="file" 사용 시 유의 사항]
1. 파일 선택 후 취소를 누르면
선택한 파일이 사라진다 (value == '')
2. value로 대입할 수 있는 값은 '' (빈칸)만 가능하다
3. 선택된 파일 정보를 저장하는 속성은
value가 아니라 files이다
*/
console 에 찍힌 파일명 C:\fakepath\maenggoo.jpg
@PostMapping("profile")
public String profile(
@RequestParam("profileImg") MultipartFile profileImg,
@SessionAttribute("loginMember") Member loginMember,
RedirectAttributes ra
) {
// 서비스 호출
// /myPage/profile/변경된파일명.확장자 형태의 문자열
// 현재 로그인한 회원의 PROFILE_IMG 컬럼값으로 수정(UPDATE)
int result = service.profile(profileImg, loginMember);
# 프로필 이미지 요청 주소 (FileConfig)
my.profile.resource-handler=/myPage/profile/**
# 프로필 이미지 요청 시 연결할 서버 폴더 경로
my.profile.resource-location=file:///C:/uploadFiles/profile/
# 서비스에서 프로필 이미지 요청 주소를 조합할 때 사용할 예정 (경로)
my.profile.web-path=/myPage/profile/
# 서비스에서 파일 저장(transferTo()) 시 사용할 폴더 경로
my.profile.folder-path=C:/uploadFiles/profile/
필드 추가
@Value("${my.profile.resource-handler}")
private String profileResourceHandler;
@Value("${my.profile.resource-location}")
private String profileResourceLocation;
public void addResourceHandlers(ResourceHandlerRegistry registry)
프로필 이미지 요청 - 서버 폴더 연결 추가
registry.addResourceHandler(profileResourceHandler) // /myPage/profile/**
.addResourceLocations(profileResourceLocation); // file:///C:/uploadFiles/profile/
file:///C: 는 파일 시스템의 루트 디렉토리
file:// 은 URL 스킴(Scheme), 파일 시스템의 리소스
/C: 는 Windows 시스템에서 C드라이브를 가리킴.
file:///C: 는 "C 드라이브의 루트 디렉토리"를 의미함
실제로 경로가 존재해야함 폴더 생성
ServiceImpl
@PropertySource("classpath:/config.properties")
public class MyPageServiceImpl implements MyPageService {
@Value("${my.profile.web-path}")
private String profileWebPath; // /myPage/profile/
@Value("${my.profile.folder-path}")
private String profileFolderPath; // C:/uploadFiles/profile/
mapper 호출
@Override
public int profile(MultipartFile profileImg, Member loginMember) {
// 수정할 경로
String updatePath = null;
// 변경명 저장
String rename = null;
// 업로드한 이미지가 있을 경우
// - 있을 경우 : 수정할 경로 조합 (클라이언트 접근 경로 + 리네임한 파일명)
if(!profileImg.isEmpty()) {
// updatePath 조합
// 1. 파일명 변경
rename = Utility.fileRename(profileImg.getOriginalFilename());
// 2. /myPage/profile/변경된파일명
updatePath = profileWebPath + rename;
}
// 수정된 프로필 이미지 경로 + 회원 번호를 저장할 DTO 객체
Member mem = Member.builder()
.memberNo(loginMember.getMemberNo())
.profileImg(updatePath)
.build();
// UPDATE 수행
int result = mapper.profile(mem);
SQL 문 실행
<update id="profile">
UPDATE "MEMBER" SET
PROFILE_IMG = #{profileImg}
WHERE MEMBER_NO = #{memberNo}
</update>
// DB에 수정 성공 시
if(result > 0) {
// 프로필 이미지를 없앤 경우(NULL로 수정한 경우)를 제외
// -> 업로드한 이미지가 있을 경우
if(!profileImg.isEmpty()) {
// 파일을 서버 지정된 폴더에 저장
profileImg.transferTo(new File(profileFolderPath + rename));
}
// 세션 회원 정보에서 프로필 이미지 경로를
// 업데이트한 경로로 변경
loginMember.setProfileImg(updatePath);
}
return result;
}
String message = null;
if(result > 0) message = "변경 성공";
else message = "변경 실패";
ra.addFlashAttribute("message", message);
return "redirect:profile"; // 리다이렉트 - /myPage/profile (상대경로)
프로필 변경확인하기