본문 바로가기
[ Project ]/Team

[ Team ] 회원관리 - 유저 이미지 업로드 기능 구현

by 환이s 2023. 6. 30.


담당 기능

 

 

 

이전 포스팅에 이어서 관리자 회원관리 페이지에 회원 프로필 사진 업로드 기능을 구현해 보겠습니다.

 

회원관리 CRUD 기능에 대해 알아보시는 분들은 아래 포스팅을 참고해 보시면 좋을 거 같습니다!

 

 

[ Team ] 관리자 페이지 생성/ CRUD 구현 (목록/상세정보)

담당 기능 ADMIN 페이지 회원 관리 기능 구현 담당을 맡아서 책임감 갖고 기능 구현을 해보겠습니다. 프로젝트 구조는 다음과 같습니다. 기능 구현할 때 사용된 객체/메서드/패턴은 이전 포스팅에

drg2524.tistory.com

 

 

[ Team ] 관리자 페이지 생성/ CRUD 구현 (수정 페이지)

담당 기능 이전 관리자 페이지 CRUD 기능 추가 이어서 수정 페이지 기능 구현을 해보겠습니다. 프로젝트 구조는 다음과 같습니다. 기능 구현할 때 사용된 객체/메서드/패턴은 이전 포스팅에서 자

drg2524.tistory.com

 

 

[ Team ] 회원관리 - 페이지네이션 처리 기능 구현

담당 기능 이전 포스팅에서 관리자 페이지 CRUD 기능 구현 코드 및 해석에 대해서 알아봤습니다. CRUD 기능에 대해서 알아보시는 분들은 아래 포스팅을 참고해 보시면 좋을 거 같습니다! [ Team ] 관

drg2524.tistory.com

 

 

프로젝트 구조는 다음과 같습니다.


프로젝트 구조

 


라이브러리 추가

 

이미지 업로드 기능 구현을 하려면 필요한 라이브러리가 있기 때문에 추가해 줍니다.

 

		<!—파일업로드 관련 라이브러리 —>
		<!— https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload —>
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.3</version>
		</dependency>
        
        
		<!— 이미지 썸네일을 만들어주는 라이브러리 —>
		<!— https://mvnrepository.com/artifact/org.imgscalr/imgscalr-lib —>
		<dependency>
			<groupId>org.imgscalr</groupId>
			<artifactId>imgscalr-lib</artifactId>
			<version>4.2</version>
		</dependency>

 

회원관리 페이지에 필요한 라이브러리를 두 개 추가했는데,

 

'commons-fileupload' 라이브러리는 java 웹 애플리케이션에서 파일 업로드 처리를 지원해 줍니다.

또한 사용자가 업로드한 파일을 처리하고 저장할 수 있습니다.

 

'imgscalr-lib' 라이브러리는 java에서 이미지 축소판을 만드는 기능을 제공해 줍니다. 다양한 이미지 크기 조정 및 크기 조정 옵션을 제공해 줍니다.

 

라이브러리 추가 후 업데이트를 끝내고 Controller부터 코드 작업을 시작하는데, 회원관리 목록 페이지에서 수정 페이지로 이동했을 때, 이미지를 추가할 수 있도록 로직을 작성합니다.

 


Controller

 

	@RequestMapping("/userUpdate")
	public String UpdateUser(
			@ModelAttribute MemberDTO dto,
			HttpSession session,
			@RequestParam(value = "file", required = false) MultipartFile file,
			HttpServletRequest request) throws Exception {


		// 기존 이미지 URL 가져오기
		String existingImageUrl = (String) session.getAttribute("picture_url");

		// 이미지가 선택되지 않았을 때 기존 파일 유지
		if (file == null || file.isEmpty()) {
			dto.setPicture_url(existingImageUrl);
		} else {
			// 이미지가 선택되었을 때의 로직은 여기에 작성

			// 파일 저장 경로 및 URL 설정
			String uploadFolder = "c:\\\\upload\\\\";
			UUID uuid = UUID.randomUUID();
			String[] uuids = uuid.toString().split("-");
			String uniqueName = uuids[0];
			String fileExtension = "";
			int lastDotIndex = file.getOriginalFilename().lastIndexOf(".");
			if (lastDotIndex >= 0 && lastDotIndex < file.getOriginalFilename().length() - 1) {
				fileExtension = file.getOriginalFilename().substring(lastDotIndex + 1);
			}
			String imageUrl = request.getContextPath() + "/picture_url/" + uniqueName +"."+ fileExtension;

			// 파일 저장
			File saveFile = new File(uploadFolder + "/" + uniqueName +"." + fileExtension);
			try {
				file.transferTo(saveFile);
			} catch (IOException e) {
				e.printStackTrace();
			}

			// 업데이트된 이미지 URL 설정
			dto.setPicture_url(imageUrl);

			// 이전 파일 삭제 (선택적으로 진행)
			if (existingImageUrl != null && !existingImageUrl.isEmpty()) {
				String existingFileName = existingImageUrl.substring(existingImageUrl.lastIndexOf("/") + 1);
				File existingFile = new File(uploadFolder + "/" + existingFileName);
				existingFile.delete();
			}
		}

		// 나머지 로직은 동일하게 유지

		// 비밀번호 암호화
		String encodedPassword = passwordEncoder.encode(dto.getMem_pass());
		dto.setMem_pass(encodedPassword);

		adminService.updateMember(dto);

		// 세션에 업데이트된 회원 정보 반영
		MemberDTO updatedMember = adminService.getupdateMember(dto.getMem_email());
		session.setAttribute("mem_name", updatedMember.getMem_name());
		session.setAttribute("mem_nickname", updatedMember.getMem_nickname());
		session.setAttribute("mem_zipcode", updatedMember.getMem_zipcode());
		session.setAttribute("mem_address1", updatedMember.getMem_address1());
		session.setAttribute("mem_address2", updatedMember.getMem_address2());
		session.setAttribute("mem_phone", updatedMember.getMem_phone());
		session.setAttribute("picture_url", updatedMember.getPicture_url());

		return "redirect:/admin/admin_listPage?num=1";
	}

 

위 코드를 보시면 "/userUpdate" URL 요청이 들어오면 파일 업로드 처리 및 사용자 업데이트 작업을 진행할 수 있도록 코드 구현을 했습니다.

 

Session에서 "picture_url"이라는 키를 사용하여 기존 이미지 URL을 가져오고, 업로드된 파일이 제공되지 않거나 비어있는 경우, 기존 이미지 URL이 DTO 객체에 설정되어 현재 프로필 사진을 유지합니다.

 

만약 파일이 제공된 경우 "else" 블록 내의 로직이 실행되는데, 

 

먼저 파일 저장 경로와 URL을 글쓴이는 "C:\\upload\\"로 설정했고, "UUID.randomUUID()"를 사용하여 파일의 고유한 이름을 생성합니다.

 

그리고 "file.transferTo(saveFile)"를 사용하여 파일을 지정된 업로드 폴더에 저장해 주고, DTO 객체의 사진 URL이 새로운 이미지 URL로 update 됩니다.

 

여기서 추가적인 디테일 로직을 추가하자면 선택 사항 코드를 작성했는데, 기존 이미지 URL이 존재하고 비어 있지 않은 경우, 이전 프로필 사진 파일을 삭제할 수 있게 구현했습니다.

 

마지막으로 비밀번호를 암호화 처리하는 코드를 작성하고 DB에 요청을 보냅니다.

 


Service

 

회원 정보 수정 기능을 수행하고, 수정된 회원 정보를 DB에서 가져오기 위해 DAO 메서드를 호출합니다.

 

    // 수정 기능 추가

    public void updateMember(MemberDTO dto) {
        memberDao.adminupdateMember(dto);
    }
    
    
        @Override
    public MemberDTO getupdateMember(String mem_email) {
        return memberDao.viewMember(mem_email);
    }

 


DAO

 

DAO에서는 MyBatis를 사용하여 쿼리문을 실행하기 위해 mapper 파일과 연결해 줍니다.

 

@Override
	public void adminupdateMember(MemberDTO dto) {
		sqlSession.update("admin.updateMember" , dto);
	}

	@Override
	public MemberDTO viewMember(String mem_email) {
		return sqlSession.selectOne("member.memberView", mem_email);
	}

 


mapper

 

    <update id="updateMember">
    update member
    set mem_name=#{mem_name},
        mem_nickname=#{mem_nickname},
        mem_zipcode=#{mem_zipcode}
    ,mem_address1=#{mem_address1},
        mem_address2=#{mem_address2},
        picture_url = #{picture_url, jdbcType=VARCHAR}, mem_edit_date=SYSDATE
    where mem_num=#{mem_num}
    </update>



    <select id="memberView" resultType="member">
        SELECT *
        FROM member
        WHERE mem_email = #{mem_email}
    </select>

 

쿼리문까지 끝내면 뒷단 코딩은 끝났습니다.

 

다음으로는 앞단에 이미지를 띄우기 위한 작업을 해야 하는데, 이미지 업로드 기능은 대부분 경로 부분에서 에러가 많이 발생합니다.. ㅎ

 

경로 설정을 위한 properties 파일을 생성합니다.


upload.properties

 

# upload.properties
upload.path=c:/upload

 

설정한 경로를 servlet-context.xml에 파일 업로드 속성과 같이 빈 등록합니다.

 


serviet-context.xml

 

<!-- 파일업로드를 위한 디렉토리 설정 -->
<beans:bean id="uploadPath" class="java.lang.String">
   <beans:constructor-arg value="classpath:upload.properties" />
</beans:bean>

<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
   <beans:property name="maxUploadSize" value="100000000"/>
</beans:bean>
<beans:bean id="uploadProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
   <beans:property name="location" value="classpath:upload.properties"/>
</beans:bean>

 

uploadPath bean:

String 클래스를 사용하여 upload.properties 파일의 경로를 설정해 주고,
constructor-arg 요소를 사용하여 classpath:upload.properties 값을 전달합니다.



multipartResolver bean:

파일 업로드를 처리하는 데 사용되는 CommonsMultipartResolver 클래스를 나타냅니다.
maxUploadSize 속성을 사용하여 최대 업로드 크기를 설정합니다. 여기서는 100000000바이트(약 100MB)로 설정되어 있습니다.



uploadProperties bean:

PropertiesFactoryBean 클래스를 사용하여 upload.properties 파일을 로드하는 데 사용됩니다.
location 속성을 사용하여 classpath:upload.properties 파일의 위치를 지정합니다.

 


VIEW

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:set var="path" value="${pageContext.request.contextPath}"/>
<script src="${path}/resources/include/js/bootstrap.js"></script>
<link rel="stylesheet" href="${path}/resources/include/style.css">
<script src="${path}/resources/include/jquery-3.6.3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
 <meta charset="UTF-8">
 <title>adminList</title>
 <script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
 <script type="text/javascript">
  function daumZipCode() {
   new daum.Postcode({
    oncomplete: function(data) {
     var addr = ''; // 주소 변수
     var extraAddr = ''; // 참고항목 변수
     if (data.userSelectedType === 'R') {
      addr = data.roadAddress;
     } else {
      addr = data.jibunAddress;
     }
     if(data.userSelectedType === 'R'){
      if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
       extraAddr += data.bname;
      }
      if(data.buildingName !== '' && data.apartment === 'Y'){
       extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
      }
      if(extraAddr !== ''){
       extraAddr = ' (' + extraAddr + ')';
      }
      document.getElementById("mem_address1").value = extraAddr;
     } else {
      document.getElementById("mem_address2").value = '';
     }
     document.getElementById('mem_zipcode').value = data.zonecode;
     document.getElementById("mem_address1").value = addr;
     document.getElementById("mem_address2").focus();
    }
   }).open();
  }
 </script>
 <style>
  body {
   font-family: Arial, sans-serif;
  }


  h2 {
   padding: 20px;
   text-align: center;
   color: #333;
  }

  table {
   width: 600px;
   margin: 0 auto;
   border-collapse: collapse;
  }

  table td {
   padding: 8px;
  }

  #profileimg{
   text-align: center;
  }

  input[type="email"],
  input[type="password"],
  input[type="text"]{
   width: 100%;
   padding: 8px;
   border: 1px solid #ccc;
   border-radius: 4px;
  }

  button {
   width: 100%;
   padding: 8px 16px;
   background-color: #4CAF50;
   color: #fff;
   border: none;
   border-radius: 4px;
   cursor: pointer;
  }

  .button-container {
   display: flex;
   justify-content: space-between;
   margin-top: 16px;
  }

  .button-container button {
   width: 48%;
  }
  #container {
   display: flex;
   height: 100%;
   width: 100%;
   flex-direction: column;
  }

  #category {
   display: flex;
   flex-direction: column;
   height: 100%;
   width: 15%;
   gap: 25px;
   align-items: center;
   border-right: 1px solid #000000;
   padding-top: 50px;
  }
  div a.menubar {
   text-decoration: none;
   display: flex;
   color: #000;
   padding: 25px 25px 25px 25px;
   font-weight: bold;
  }
  .menu > a:hover {
   background-color: #333;
   color: #fff;
  }
 </style>
</head>
<body>
<%@ include file="../header.jspf" %>

<div id="container">
 <div style="display: flex; height: auto;">
  <div id="category" class="menu" style="width:10%; height:auto;">
   <a class="menubar" href="${path}/admin/admin_listPage?num=1">회원관리</a>
   <a class="menubar" href="${path}/trip/trip_list_admin?num=1">관광명소 관리</a>
   <a class="menubar" href="${path}/review/list?num=1" >리뷰리스트 관리</a>
   <a class="menubar" href="${path}/faq/listPage?num=1">FAQ</a>
  </div>

<h2 style="font-size: 20px; margin-left: 100px;">프로필 수정</h2>
<form name="form1" id="form1" method="post" style="text-align: left; padding-left: 100px; margin-top: 80px; " enctype="multipart/form-data">
 <table  width="600px" >
  <div style="text-align: center;">
      <label for="file-input">
          <img id="previewImage" style="width: 200px; height: 200px; cursor: pointer;" src="${picture_url}" class="img-thumbnail rounded-circle">
      </label>
          <input id="file-input" type="file" name="file" style="display: none;">
  </div>
  <tr>
   <td>이메일</td>
   <td>이름</td>
  </tr>
  <tr>
   <td><input type="email" id="mem_email" name="mem_email" value="${dto.mem_email}" readonly></td>
   <td><input type="text" id="mem_name" name="mem_name" value="${dto.mem_name}"></td>
  </tr>

  <tr>
   <td>비밀번호</td>
   <td >비밀번호 확인</td>
  </tr>
  <tr>
   <td><input type="password" id="mem_pass" name="mem_pass" value="${dto.mem_pass}" readonly></td>
   <td><input type="password" id="passwd_ck" name="passwd_ck" value="${dto.mem_pass}"  readonly></td>

  <tr>
   <td>닉네임</td>
   <td>전화번호</td>
  </tr>
  <tr>
   <td><input type="text" id="mem_nickname" name="mem_nickname" value="${dto.mem_nickname}" readonly></td>
   <td><input type="text" id="mem_phone" name="mem_phone" value="${dto.mem_phone}"></td>
  </tr>
  <tr>
   <td style="text-align:center;">우편번호</td>
   <td><input type="text" id="mem_zipcode" name="mem_zipcode" onclick="daumZipCode()" value="${dto.mem_zipcode}" placeholder="우편번호 찾기" readonly></td>
  </tr>
  <tr>
   <td colspan="2"><input type="text" id="mem_address1" name="mem_address1" value="${dto.mem_address1}" readonly></td>
  </tr>
  <tr>
   <td colspan="2"><input type="text" id="mem_address2" name="mem_address2" value="${dto.mem_address2}" placeholder="상세주소를 입력해주세요."></td>
  </tr>
     <tr>
      <td colspan="2">
       <input type="hidden" name="mem_num" value="${dto.mem_num}">
        <button type="button" id="userUpdate">수정하기</button>
      </td>
     </tr>
  <tr>
   <td colspan="2"><button type="button" id="logback" name="logback">목록</button>
    <div style="color: red;">${message}</div></td>
  </tr>

 </table>
</form>
 </div>
 <%@include file="../footer.jspf" %>
<script>
 $("#logback").click(function (){
  location.href="${path}/admin/memberList";

 });


 $("#userUpdate").click(function (){

  var mem_name = $("#mem_name").val();
  var mem_nickname = $("#mem_nickname").val();
  var mem_phone = $("#mem_phone").val();
  var mem_zipcode = $("#mem_zipcode").val();
  var mem_address1 = $("#mem_address1").val();
  var mem_address2 = $("#mem_address2").val();


  if(mem_name == ""){
   alert("이름은 필수입니다.");
   $("#mem_name").focus();
   return;
  }
  if(mem_nickname == ""){
   alert("닉네임을 입력하세요.");
   $("#mem_nickname").focus();
   return;
  }
  if(mem_phone == ""){
   alert("번호를 입력하세요.");
   $("#mem_phone").focus();
   return;
  }
  if(mem_zipcode == ""){
   alert("우편번호를 입력하세요.");
   $("#mem_zipcode").focus();
   return;
  }
  if(mem_address2 == ""){
   alert("상세주소를 입력하세요.");
   $("#mem_address2").focus();
   return;
  }

  if(confirm("정보 수정 완료")){

   document.form1.action="${path}/admin/userUpdate";
   document.form1.submit();
  }

 });

 const fileInput = document.getElementById('file-input');
 const previewImage = document.getElementById('previewImage');

 fileInput.addEventListener('change', function(event) {
  const file = event.target.files[0];
  const reader = new FileReader();

  reader.onload = function(e) {
   previewImage.src = e.target.result;
  };

  reader.readAsDataURL(file);
 });


 function back() {
  history.back();
 }

</script>

</div>
</body>
</html>

 


구현 결과

 


마치며

 

오늘은 회원 수정 페이지 이미지 업로드 및 수정 코드 리팩토링을 해보았습니다.

다음 포스팅에서 뵙겠습니다.

 

728x90