파일 업로드 구현

DeadWhale·2022년 6월 1일
0

Servlet/JSP

목록 보기
18/22
post-thumbnail

Community 웹 사이트 프로젝트 진행중 프로필이미지 업로드를 구현 부분의 수업 내용을 정리한 글

메인 페이지 / 프로필 페이지 / 댓글 리스트에서 조회가 가능하다
이미지를 보여주는건 쉽지만 이미지를 DB서버로 업로드하는건 어렵다

DB에 업로드하는 방법이 매우 어려웠다

고려해야되는 부분이 여러개 있었는데 생각나는 것만 추려보자면

  • 파일이 서블릿으로 오면 기본적으로
    application/x-www-form-urlencoded으로 문자열 인코딩되서 String으로 Parameter를 얻어올 수 있는데 이미지 데이터는 바이트코드(완전01로 이루어진)로 얻어와야 해 form 내부의 인코딩 방식을 변경해 전달해야한다. enctype="multipart/form-data"

  • input type= "file"은 들어온 자료의 fakePath + 업로드된 원본 파일명이 보이게 된다

    이 주소는 브라우저가 만든 가상의 주소이다.

  • 만약 type="file"에 입력을 한번 더 번복하게 되면
    다시 value에 공백이 들어온다

    이건 브라우저의 보안정책이라고 하는 것 같다
    취약점이 된 적이 있는 것 같다.

전체적으로 코드 리뷰

  • 이미지를 업로드 하기 위해 form태그의 인코딩을 변경한다.
 <form action="profile" method="POST" name="myPage-form"
enctype="multipart/form-data" onsubmit="return profileValidate()">
  • 프로필이 서버로 전송하기 전 프로필 이미지를 다시 기본 이미지로 변경 할 수 있는데 이를 체크하기 위한 함수
 <!-- 삭제버튼이 눌린걸 기록하는 숨겨진 input 태그 -->
<!-- 0:안눌려짐  1:눌려짐-->
<input type="hidden" name="delete" id="delete" value="0" >

바로 윗 코드 처럼 프로필 이미지를 비우기 위해서는
삭제 x 버튼이 눌릴 때 함수를 호출해 프로필 이미지를 비우는 input태그에 null을 대입해줘야 한다.

/* 프로필 이미지 옆 x 버튼 클릭시 */
document.getElementById("delete-image").addEventListener("click",function(){
    const del = document.getElementById("delete");
    //0: 안눌러짐
    //1: 눌러짐
    if(del.value==0){ //눌러지지 않은 경우 수행
    //1) 프로필 이미지를 기본 이미지로 변경
    document.getElementById("profile-image").setAttribute("src",contextPath+"/resources/images/user.png")
    //2) input type = " file " 에 저장된 value에 빈문자열을 대입
    inputImage.value="";
    del.value=1; //이제 눌려진걸로 인식.
    }
})

그 다음 submit 동작 수행 전 확인하는 함수

/* 이미지 선택을 확인 */
function profileValidate(){
    /* 히든 타입 */
    const del = document.getElementById("delete");
    if(inputImage.value == "" && del.value==0){ //빈문자열 == 파일선택이 안된 상태
        //아무것도 안하고 / 프로필 삭제 버튼도 누르지 않은 경우.
        alert("이미지를 선택 후 변경해주세요");
        return false;
    }
    return true;
}

만약 inputImage가 공백이고 삭제버튼이 눌린적이 없는 상태면
아무런 동작도 하지 않고 전송할려 한 것임으로 false를 반환해 submit을 제한한다.

작성할 때 순서가 약간 다르긴했지만
이번에는 inputImage에 change 이벤트가 발생할 때
이미지 블럭에 이미지를 미리 보여주는 함수이다.

//회원 프로필 이미지 변경  (미리보기)
const inputImage = document.getElementById("input-image");
if(inputImage != null){
    //input type = "file" 요소는 파일이 선택 될 때 change 이벤트가 발생한다.
    inputImage.addEventListener("change",function(){
        // this : 이벤트가 발생한 요소 == <input=file 태그>
        // filse : input type ="file"만 사용 가능한 속성
        //          선택된 파일 목록(배열 형태)을 반환
       
         //파일 목록에서 첫번째 파일객체를 얻어옴
         if(this.files[0] !=undefined){//파일이 선택되었을 때.
            const reader = new FileReader();
            //자바 스크립트의 FIle Reader 
            // - 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 사용하는 객체.

            reader.readAsDataURL(this.files[0]);
             //reader.readAsDataURL( 파일 )  
             // - 지정된 파일의 내용을 읽기 시작함.
             // - 읽어오는게 완료되면 result 속성 data:에 
             //   읽어온 파일의 위치를 나타내는 URL이 포함된다.
             //   이렇게 일어온 URL을 이용해 브라우저에 이미지를 볼 수 있다.

             //   data:url(파일경로);  

            //FileReader.onload = function(){}
            // - on + load : 다 읽어왔을 때
            // - FileReader가 파일을 다 읽어온 경우. 함수를 수행.

             reader.onload = function(e){ //고정 이벤트 모델
                // e: 이벤트 발생 객체
                // e.targer : 이벤트가 발생한 요소(객체) -> FileReader
                // e.target.result : FileReader가 읽어온 파일의 URL

                //프로필 이미지 scr 속성의 값을 FIleReader가 읽어온 파일의 URL로 변경
                const profileImage = document.getElementById("profile-image");
                profileImage.setAttribute("src",e.target.result);
                // setAttribute () 호출 시 중복되는 속성이 존재하면 덮어쓰기된다.
                document.getElementById("delete").value=0;
                //새로운 이미지가 선택되었기 때문에 1->0으로 x버튼을 안눌러진 상태로 변경
             };
         }
    })
}

이것 저것 따질 것 없이 입력된 첫번째 파일이 있을 경우 FileReader()함수를 사용해. 파일정보를 추출해 낸 후 이미지 파일의 데이터경로를 로드한 후 모든 읽어오기가 끈났을때
프로필 이미지를 변경하는 함수를 수행한다.
이때 읽어온 데이터 경로에서 .result를 이용해야지 uri를 대입할 수 있다

profileImage.setAttribute("src",e.target.result);
src 속성에 이미지의 경로를 대입해 미리보기를 수행한다 document.getElementById("delete").value=0;
그 후 기본이미지 변경을 확인하기 위한 del을 바꼈다는 의미인 0을 대입한다.


위에서는 js와 jsp에서 수행된 코드이고 Servlet 클래스에서도 오랜만에 할 일이 많았다.

해야 되는 것

  • 바이트 코드로 넘어와 파라미터로 문자열을 받을 수 없는데. 이를 처리하는 처리과정이 필요 COS.Jar 라이브러리를 활용해 MulitPartRequest 객체를 만들어야한다.

cos 라이브러리를 활용 전 5가지 과정이 필요하다.
1. 업로드 되는 파일 크기의 전체 합을 지정(Byte 단위)

int maxSize = 1024 * 1024 * 20; 
			 //1kb    1MB   20MB

2.업로드 되는 파일이 어디에 저장되는지 경로 지정 필요
이 때 경로는 절대 경로여야 한다.

server 옵션 중 Serve Modules Without Publishing 체크 필요

그 다음 Resources/images/memberProfile의 경로를 얻어와야 한다.

HttpSession session = req.getSession(); //session 얻어오는것은 상관없음 ( 사용 가능 )
// 최상위 경로 ( " / "  == webapp 폴더 )의 컴퓨터 상의 실제 절대 경로를 얻어옴.
String root = session.getServletContext().getRealPath("/");
//실제 파일이 저장되는 폴더의 경로
String folderPath = "/resources/images/memberProfile/";
	
// memberProfile 폴더까지의 절대 경로
String filePath = root+folderPath;

3.수만가지 파일이 올라오는데 이 파일의 이름이 중복되지 않게 하기 위한 이름 명명 규칙을 제정해야 한다.

MyRenamePolicy 객체 생성

cos.jar에서 제공해주는
FileRenamePolicy 인터페이스를 상속 받아야한다.

//파일명 변경 정책.
public class MyRenamePolicy implements FileRenamePolicy {

	@Override
	public File rename(File originalFile) {

		long currentTime = System.currentTimeMillis();
		// 1970년 1월 1일 오전9시 부터 현재시간까지의 ms

		SimpleDateFormat ft = new SimpleDateFormat("yyyyMMddHHmmss");
		int ranNum = (int) (Math.random() * 100000); // 5자리 랜덤 숫자 생성
		// 0<= 난수 <= 99,999

		String str = "_" + String.format("%05d", ranNum);
		// 앞에 0이 나와서 사라져도 앞에 0을 추가해줌.

		// String.format : 문자열을 지정된 패턴의 형식으로 변경하는 메소드
		// %05d : 오른쪽 정렬된 십진 정수(d) 5자리(5)형태로 변경. 빈자리는 0으로 채움(0)

		// 파일명을 변경해도 확장자를 유지하기 위하여
		// 별도로 확장자 명 가져오기
		String name = originalFile.getName();
		String ext = null;

		int dot = name.lastIndexOf(".");

		if (dot != -1) {
			// dot 포함해서 확장자 추출 (ext)
			ext = name.substring(dot);
		} else {
			// 확장자가 없는 경우
			ext = "";
		}

		String fileName = ft.format(new Date(currentTime)) + str + ext;
		// 202205311221235 + _랜덤난수 5자리 + 확장자.
		File newFile = new File(originalFile.getParent(), fileName);

		return newFile;
	}
}

4.파일 데이터들은 문자열이니 utf-8으로 다시 설정해야한다


5.MulitpartRequest를 생성
//Mult part Request객체가 생성됨과 동시에 지정된 경로에 
//지정된 파일명 변경 정책에 맞게 이름이 바뀐 파일이 저장(upload)된다.
MultipartRequest mpReq = new MultipartRequest(req, filePath, maxSize, encoding, new MyRenamePolicy());
											/개체  저장경로   사진최대크기  인코딩방식      이름변경 함수

// 2) 업로드된 이미지의 웹 접근 경로(folderPath에 저장되어있다) + 변경된 파일명.

// -> 원복 파일명
//getOriginalyFileName("input type="file" 의 name속성값)
// -> 변경된 파일명
//getFilesystemName("input type="file" 의 name속성값)

//DB에 대입될 프로필 이미지 경로.
//단 x버튼이 클릭된 상태면 null을 가지게 한다.
String profileImage = folderPath+mpReq.getFilesystemName("profileImage");

//프로필 이미지 삭제
// 1) "delete" input type = "hidden" 태그의 값(파라미터 얻어오기)
// **(주의!)** multipart/from-data 형식의 요청을 처리 중이기 때문에
// req를 이용해 parameter를 얻어올 수 없다(Byte코드이기때문에)
//  위에서 정의한 mpReq를 이용해 확인 가능
int delete = Integer.parseInt(mpReq.getParameter("delete"));

// 2) delete의 값이 1(눌러진경우) 프로필 이미지의 값을 Null로 반환
// DB에서 프로필이미지 경로가 NUll이여야지 기본이미지를 JSP에서 보여주는 If가 구성되어있다.
if(delete ==1) {
	profileImage = null;
}

위 모든 요청이 처리후 DB까지 갔다 와 Redirect한다

//프로필 이미지 변경 Service 호출후 결과 반환 받기
MemberService service = new MemberService(); 

int result = service.updateProfileImage(memberNo,profileImage);

if(result>0){ //DB 업데이트 수행 성공했을때
	session.setAttribute("message", "프로필 이미지가 변경되었습니다");
	//DB의 프로필 이미지 정보는 변경 되었으나 
	//session에 저장된 로그인 정보중 프로필 이미지는 변경되지 않은
	//동기화 작업 필요
	loginMember.setProfileImage(profileImage);
}else {
	session.setAttribute("message", "프로필 이미지가 변경 실패");
}

//성공 실패 관계 없이 프로필에 화면 재요청
//resp.sendRedirect("GET방식");
resp.sendRedirect("profile");

하 파일 업로드 어렵다 다시 해석해야 되는 부분도 많은 것 같다

0개의 댓글