Spring Boot Board Project_16 프로필 이미지 변경

송지윤·2024년 4월 21일
0

Spring Framework

목록 보기
49/65

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이다
*/

1. Controller 에서 값 받아서 서비스 호출

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);

2. 이미지 경로도 다 config.properties 에 저장

# 프로필 이미지 요청 주소 (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/

2-2. FileConfig 클래스

필드 추가

	@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 드라이브의 루트 디렉토리"를 의미함

실제로 경로가 존재해야함 폴더 생성

2-3. ServiceImpl config.properties 폴더 읽어와서 사용

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>

3. ServiceImpl 로 결과값 가지고 돌아와 분기처리

		// DB에 수정 성공 시
		if(result > 0) {
			
			// 프로필 이미지를 없앤 경우(NULL로 수정한 경우)를 제외
			// -> 업로드한 이미지가 있을 경우
			if(!profileImg.isEmpty()) {
				// 파일을 서버 지정된 폴더에 저장
				profileImg.transferTo(new File(profileFolderPath + rename));
			}
			
			// 세션 회원 정보에서 프로필 이미지 경로를
			// 업데이트한 경로로 변경
			loginMember.setProfileImg(updatePath);
		}
		
		return result;
	}

Controller 분기처리

		String message = null;
		
		if(result > 0) message = "변경 성공";
		else message = "변경 실패";
		
		ra.addFlashAttribute("message", message);
		
		return "redirect:profile"; // 리다이렉트 - /myPage/profile (상대경로)

프로필 변경확인하기

0개의 댓글