SpringBoot : 마이페이지, 파일 업로드
<!-- 왼쪽 사이드 메뉴 -->
<section class="left-side">
사이드메뉴
<ul class="list-group">
<li> <a th:href="@{/myPage/profile}">프로필</a> </li>
<li> <a th:href="@{/myPage/info}">내 정보</a> </li>
<li> <a th:href="@{/myPage/changePw}">비밀번호 변경</a> </li>
<li> <a th:href="@{/myPage/secession}">회원 탈퇴</a> </li>
</ul>
</section>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Page</title>
<link rel="stylesheet" th:href="@{/css/myPage/myPage-style.css}"> <!-- static 하위 폴더 기준 -->
</head>
<body>
<main>
<th:block th:replace="~{/common/header}"></th:block>
<!-- 마이페이지 - 내 정보 -->
<section class="myPage-content">
<!-- 사이드메뉴 include -->
<th:block th:replace="~{/myPage/sideMenu}"></th:block>
<!-- 오른쪽 마이페이지 주요 내용 부분 -->
<section class="myPage-main">
<h1 class="myPage-title">비밀번호 변경</h1>
<span class="myPage-subject">현재 비밀번호가 일치하는 경우 새 비밀번호로 변경할 수 있습니다.</span>
<form th:action="@{changePw}" method="POST" name="myPageFrm" id="changePwFrm">
<div class="myPage-row">
<label>현재 비밀번호</label>
<input type="password" name="currentPw" id="currentPw" maxlength="30" >
</div>
<div class="myPage-row">
<label>새 비밀번호</label>
<input type="password" name="newPw" id="newPw" maxlength="30">
</div>
<div class="myPage-row">
<label>새 비밀번호 확인</label>
<input type="password" name="newPwConfirm" id="newPwConfirm" maxlength="30">
</div>
<button class="myPage-submit">변경하기</button>
</form>
</section>
</section>
</main>
<th:block th:replace="~{/common/footer}"></th:block>
<script th:src="@{/js/myPage/myPage.js}"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>마이페이지</title>
<link rel="stylesheet" th:href="@{/css/myPage/myPage-style.css}">
</head>
<body>
<main>
<th:block th:replace="~{/common/header}"></th:block>
<section class="myPage-content">
<!-- 사이드메뉴 include -->
<th:block th:replace="~{/myPage/sideMenu}"></th:block>
<section class="myPage-main">
<h1 class="myPage-title">내 정보</h1>
<span class="myPage-subject">원하는 회원 정보를 수정할 수 있습니다.</span>
<form th:action="@{info}" method="POST" name="myPageFrm" id="updateInfo">
<div class="myPage-row">
<label>닉네임</label>
<input type="text" name="memberNickname" maxlength="10"
value="" id="memberNickname"
>
</div>
<div class="myPage-row">
<label>전화번호</label>
<input type="text" name="memberTel" maxlength="11"
value="" id="memberTel"
>
</div>
<div class="myPage-row info-title">
<span>주소</span>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="우편번호"
id="postcode" value=""
>
<button type="button" onclick="postCode()">검색</button>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="도로명/지번 주소"
id="address" value=""
>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="상세 주소"
id="detailAddress" value=""
>
</div>
<button class="myPage-submit">수정하기</button>
</form>
</section>
</section>
</main>
<th:block th:replace="~{/common/footer}"></th:block>
<!-- 다음 주소 api 추가 -->
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
function postCode() {
new daum.Postcode({
oncomplete: function(data) {
// 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
// 각 주소의 노출 규칙에 따라 주소를 조합한다.
// 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
var addr = ''; // 주소 변수
//사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
addr = data.roadAddress;
} else { // 사용자가 지번 주소를 선택했을 경우(J)
addr = data.jibunAddress;
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
document.getElementById('postcode').value = data.zonecode;
document.getElementById("address").value = addr;
// 커서를 상세주소 필드로 이동한다.
document.getElementById("detailAddress").focus();
}
}).open();
}
</script>
<script th:src="@{/js/myPage/myPage.js}"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Page</title>
<link rel="stylesheet" th:href="@{/css/myPage/myPage-style.css}">
</head>
<body>
<main>
<th:block th:replace="~{/common/header}"></th:block>
<!-- 마이페이지 - 내 정보 -->
<section class="myPage-content">
<!-- 사이드메뉴 include -->
<th:block th:replace="~{/myPage/sideMenu}"></th:block>
<!-- 오른쪽 마이페이지 주요 내용 부분 -->
<section class="myPage-main">
<h1 class="myPage-title">프로필</h1>
<span class="myPage-subject">프로필 이미지를 변경할 수 있습니다.</span>
<form th:action="@{profile}" method="POST" name="myPageFrm" id="profileFrm" enctype="multipart/form-data">
<div class="profile-image-area">
<!-- 프로필 이미지가 없으면 기본 이미지 -->
<img th:src="@{/images/user.png}" id="profileImage">
<!-- 프로필 이미지가 있으면 저장된 이미지 -->
</div>
<span id="deleteImage">x</span>
<div class="profile-btn-area">
<label for="imageInput">이미지 선택</label>
<input type="file" name="profileImage" id="imageInput" accept="image/*">
<button>변경하기</button>
</div>
<div class="myPage-row">
<label>이메일</label>
<span>로그인 회원 이메일</span>
</div>
<div class="myPage-row">
<label>가입일</label>
<span>로그인 회원 가입일</span>
</div>
</form>
</section>
</section>
</main>
<th:block th:replace="~{/common/footer}"></th:block>
<script th:src="@{/js/myPage/myPage.js}"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Page</title>
<link rel="stylesheet" th:href="@{/css/myPage/myPage-style.css}">
</head>
<body>
<main>
<th:block th:replace="~{/common/header}"></th:block>
<!-- 마이페이지 - 내 정보 -->
<section class="myPage-content">
<!-- 사이드메뉴 include -->
<th:block th:replace="~{/myPage/sideMenu}"></th:block>
<!-- 오른쪽 마이페이지 주요 내용 부분 -->
<section class="myPage-main">
<h1 class="myPage-title">회원 탈퇴</h1>
<span class="myPage-subject">현재 비밀번호가 일치하는 경우 탈퇴할 수 있습니다.</span>
<form th:action="@{secession}" method="POST" name="myPageFrm" id="secessionFrm">
<div class="myPage-row">
<label>비밀번호</label>
<input type="password" name="memberPw" id="memberPw" maxlength="30">
</div>
<div class="myPage-row info-title">
<label>회원 탈퇴 약관</label>
</div>
<pre class="secession-terms">
제1조
이 약관은 샘플 약관입니다.
① 약관 내용 1
② 약관 내용 2
③ 약관 내용 3
④ 약관 내용 4
제2조
이 약관은 샘플 약관입니다.
① 약관 내용 1
② 약관 내용 2
③ 약관 내용 3
④ 약관 내용 4
</pre>
<div>
<input type="checkbox" name="agree" id="agree">
<label for="agree">위 약관에 동의합니다.</label>
</div>
<button class="myPage-submit">탈퇴</button>
</form>
</section>
</section>
</main>
<th:block th:replace="~{/common/footer}"></th:block>
<script th:src="@{/js/myPage/myPage.js}"></script>
</body>
</html>
package edu.kh.project.myPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.member.model.dto.Member;
import jakarta.servlet.http.HttpSession;
@SessionAttributes({"loginMember"})
// 1) Model에 세팅된 값의 key와 {} 작성된 값이 일치하면 session scope로 이동
// 2) Session으로 올려둔 값을 해당 클래스에서 얻어와 사용 가능하게함
// -> @SessionAttribute(key)로 사용
@RequestMapping("/myPage")
@Controller
public class MyPageController {
@Autowired
private MypageService service;
// 내 정보 페이지로 이동
@GetMapping("/info")
public String info() {
return "myPage/myPage-info";
}
// 프로필 페이지 이동
@GetMapping("/profile")
public String profile() {
return "myPage/myPage-profile";
}
// 비밀번호 변경 페이지 이동
@GetMapping("/changePw")
public String changePw() {
return "myPage/myPage-changePw";
}
// 비밀번호 변경
@PostMapping("/changePw")
public String changePwUpdate(@SessionAttribute("loginMember") Member loginMember,
String newPw,
RedirectAttributes ra,
SessionStatus status) {
int result = service.changePwUpdate(loginMember, newPw);
String message = null;
if(result > 0) {
System.out.println("비밀번호 변경 성공");
message = "비밀번호 변경 성공";
} else {
System.out.println("비밀번호 변경 실패");
message = "비밀번호 변경 실패";
}
ra.addFlashAttribute("message", message);
status.setComplete();
return "redirect:/";
}
// 탈퇴 페이지 이동
@GetMapping("/secession")
public String secession() {
return "myPage/myPage-secession";
}
// 회원 탈퇴
@PostMapping("/memberDelete")
public String memberDelete(@SessionAttribute("loginMember") Member loginMember,
RedirectAttributes ra,
SessionStatus status) {
int result = service.memberDelete(loginMember);
String message = null;
if(result > 0) {
System.out.println("회원 탈퇴 성공");
message = "회원 탈퇴 성공";
} else {
System.out.println("회원 탈퇴 실패");
message = "회원 탈퇴 실패";
}
ra.addFlashAttribute("message", message);
status.setComplete();
return "redirect:/";
}
// 회원 정보 수정
@PostMapping("/info")
public String updateInfo(@SessionAttribute("loginMember") Member loginMember,
Member updateMember,
String[] memberAddress,
RedirectAttributes ra) {
/*
* @SessionAttribute("loginMember") Member loginMember
* : Session에서 얻어온 "loginMember"에 해당하는 객체를
* 매개변수 Member loginMember에 저장
*
*
* Member updateMember
* : 수정할 닉네임, 전화번호 담긴 커맨드 객체
*
*
* String[] memberAddress
* : name="memberAddress"인 input 3개의 값(주소)
*
*
* RedirectAttributes ra : 리다이렉트 시 값 전달용 객체
*
* */
// 주소 하나로 합치자 (우편번호^^^주소^^^상세주소)
if(updateMember.getMemberAddress().equals(",,")) {
updateMember.setMemberAddress(null);
} else {
// updateMember 에 주소문자열 세팅
String addr = String.join("^^^", memberAddress);
updateMember.setMemberAddress(addr);
}
// 로그인한 회원의 번호를 updateMember에 세팅
updateMember.setMemberNo( loginMember.getMemberNo() );
// DB 회원 정보 수정 (update) 서비스 호출
int result = service.updateInfo(updateMember);
String message = null;
// 결과값으로 성공
if(result > 0) {
// -> 성공 시 Session에 로그인된 회원 정보도 수정(동기화)
System.out.println("내 정보 수정 성공"); // 확인용 콘솔창
loginMember.setMemberNickname( updateMember.getMemberNickname() );
loginMember.setMemberTel( updateMember.getMemberTel() );
loginMember.setMemberAddress( updateMember.getMemberAddress() );
message = "회원 정보 수정 성공";
} else {
// 실패에 따른 처리
System.out.println("내 정보 수정 실패"); // 확인용 콘솔창
message = "회원 정보 수정 실패";
}
ra.addFlashAttribute("message", message);
return "redirect:info"; // 상대경로 (절대경로로 작성하려면? /myPage/myPage-info)
}
/* MultipartFile : input type="file"로 제출된 파일을 저장한 객체
*
* [ 제공하는 메서드 ]
* - transferTo() : 파일을 지정된 경로에 저장(메모리 -> HDD/SSD)
* - getOriginalFileName() : 파일 원본명
* - getSize() : 파일 크기
*
*
* */
// 프로필 이미지 수정
@PostMapping("/profile")
public String updateProfile(
@RequestParam("profileImage") MultipartFile profileImage // 업로드 파일
, HttpSession session // 세션 객체
, @SessionAttribute("loginMember") Member loginMember
, RedirectAttributes ra // 리다이렉트 시 메세지 전달
) throws Exception{
// 웹 접근 경로 (webapp 기준)
String webPath = "/resources/images/member/";
// 실제로 이미지 파일이 저장되어야하는 서버컴퓨터 경로
String filePath = session.getServletContext().getRealPath(webPath);
// C:\workspace\7_Framework\boardProject\src\main\webapp\resources\images\member
// 프로필 이미지 수정 서비스 호출
int result = service.updateProfile(profileImage, webPath, filePath, loginMember);
String message = null;
if(result > 0) message = "프로필 이미지가 변경되었습니다";
else message = "프로필 변경 실패";
ra.addFlashAttribute("message", message);
return "redirect:profile";
}
// Ctrl + Shift + O(영어) -> import 할 수 있는 거 다 뜸! 한 번에 import 가능!
}
-> 사이드메뉴 클릭 시, 각자 맞는 페이지로 이동하면 성공!
1)
2)
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>마이페이지</title>
<link rel="stylesheet" th:href="@{/css/myPage/myPage-style.css}">
</head>
<body>
<main>
<th:block th:replace="~{/common/header}"></th:block>
<section class="myPage-content">
<!-- 사이드메뉴 include -->
<th:block th:replace="~{/myPage/sideMenu}"></th:block>
<section class="myPage-main">
<h1 class="myPage-title">내 정보</h1>
<span class="myPage-subject">원하는 회원 정보를 수정할 수 있습니다.</span>
<form th:action="@{info}" th:object="${session.loginMember}" method="POST" name="myPageFrm" id="updateInfo">
<div class="myPage-row">
<label>닉네임</label>
<input type="text" name="memberNickname" maxlength="10"
th:value="*{memberNickname}" id="memberNickname"
>
</div>
<div class="myPage-row">
<label>전화번호</label>
<input type="text" name="memberTel" maxlength="11"
th:value="*{memberTel}" id="memberTel"
>
</div>
<div class="myPage-row info-title">
<span>주소</span>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="우편번호"
id="postcode" value=""
>
<button type="button" onclick="postCode()">검색</button>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="도로명/지번 주소"
id="address" value=""
>
</div>
<div class="myPage-row info-address">
<input type="text" name="memberAddress" placeholder="상세 주소"
id="detailAddress" value=""
>
</div>
<button class="myPage-submit">수정하기</button>
</form>
</section>
</section>
</main>
<th:block th:replace="~{/common/footer}"></th:block>
<!-- 다음 주소 api 추가 -->
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
function postCode() {
new daum.Postcode({
oncomplete: function(data) {
// 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
// 각 주소의 노출 규칙에 따라 주소를 조합한다.
// 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
var addr = ''; // 주소 변수
//사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
addr = data.roadAddress;
} else { // 사용자가 지번 주소를 선택했을 경우(J)
addr = data.jibunAddress;
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
document.getElementById('postcode').value = data.zonecode;
document.getElementById("address").value = addr;
// 커서를 상세주소 필드로 이동한다.
document.getElementById("detailAddress").focus();
}
}).open();
}
</script>
<script th:inline="javascript">
// th:inline="javascript"
// - 타임리프를 script에서 사용 가능
// - 타임리프에서 출력되는 값을 JS에 맞는 자료형으로 변경 // 타임리프도 결국엔 java..
// Natural Template
// 1) js 문법 에러 발생 X
// 2) html 파일만 단독으로 열었을 때 발생하는 에러 방지
const loginMember = /*[[${session.loginMember}]]*/ "로그인한 회원정보"; // -> 이 형식으로 작성하면 js에서 타임리프 쓸 때 컴파일 에러 안남!
if(loginMember.memberAddress != null) {
const arr = loginMember.memberAddress.split("^^^");
document.querySelectorAll("input[name='memberAddress']").forEach( (item, i) => {
item.value = arr[i];
} )
}
</script>
<script th:src="@{/js/myPage/myPage.js}"></script>
</body>
</html>
package edu.kh.project.myPage.model.service;
import java.io.File;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.common.utility.Util;
import edu.kh.project.member.model.dto.Member;
import edu.kh.project.myPage.model.dao.MyPageMapper;
@Service
public class MyPageServiceImpl implements MyPageService{
@Autowired
private MyPageMapper mapper;
@Autowired // BCryptPasswordEncoder 의존성 주입(DI)
private BCryptPasswordEncoder bcrypt;
// 회원 정보 수정 서비스
@Transactional
@Override
public int updateInfo(Member updateMember) {
return mapper.updateInfo(updateMember);
}
// 프로필 이미지 수정 서비스
@Override
public int updateProfile(MultipartFile profileImage, String webPath, String filePath, Member loginMember) throws Exception {
// 프로필 이미지 변경 실패 대비
String temp = loginMember.getProfileImage(); // 기존에 가지고 있던 이전 이미지 저장
String rename = null; // 변경 이름 저장 변수
if(profileImage.getSize() > 0) { // 업로드된 이미지가 있을 경우
// 1) 파일 이름 변경
rename = Util.fileRename(profileImage.getOriginalFilename());
// 2) 바뀐 이름 loginMember에 세팅
loginMember.setProfileImage(webPath + rename);
} else { // 업로드된 이미지가 없는 경우 (x버튼)
loginMember.setProfileImage(null);
}
// 프로필 이미지 수정 DAO 메서드 호출
int result = mapper.updateProfileImage(loginMember);
if(result > 0) { // DB에 이미지 경로 업데이트 성공했다면
// 업로드된 새 이미지가 있을 경우
if(rename != null) {
// 메모리에 임시 저장되어있는 파일을 서버에 진짜로 저장하는 것
profileImage.transferTo(new File(filePath + rename));
}
} else { // 실패
// 이전 이미지로 프로필 다시 세팅
loginMember.setProfileImage(temp);
}
return result;
}
// 비밀번호 변경 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int changePw(String currentPw, String newPw, int memberNo) {
// 1. 현재 비밀번호, DB에 저장된 비밀번호 비교
// 1) 회원번호가 일치하는 MEMBER 테이블 행의 MEMBER_PW 조회
String encPw = mapper.selectEncPw(memberNo);
// 2) bcrypt.matches(평문, 암호문) -> 같으면 true -> 이 때 비번 수정
if(bcrypt.matches(currentPw, encPw)) {
Member member = new Member();
member.setMemberNo(memberNo);
member.setMemberPw(bcrypt.encode(newPw));
// 2. 비밀번호 변경(UPDATE DAO 호출) -> 결과 반환
return mapper.changePw( member );
}
// 3) 비밀번호가 일치하지 않으면 0 반환
return 0;
}
// 회원 탈퇴 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int secession(String memberPw, int memberNo) {
// 1. 회원 번호가 일치하는 회원의 비밀번호 조회
String encPw = mapper.selectEncPw(memberNo);
// 2.비밀번호가 일치하면
if(bcrypt.matches(memberPw, encPw)) {
// MEMBER_DEL_FL -> 'Y'로 바꾸고 1 반환
return mapper.secession(memberNo);
}
// 3. 비밀번호가 일치하지 않으면 -> 0 반환
return 0;
}
}
package edu.kh.project.myPage.model.dao;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import edu.kh.project.member.model.dto.Member;
@Mapper
public interface MyPageMapper {
/** 회원 정보 수정 DAO
* @param updateMember
* @return result
*/
public int updateInfo(Member updateMember);
/** 프로필 이미지 수정
* @param loginMember
* @return result
*/
public int updateProfileImage(Member loginMember);
/** 회원 비밀번호 조회
* @param memberNo
* @return encPw
*/
public String selectEncPw(int memberNo);
/** 비밀번호 변경
* @param newPw
* @param memberNo
* @return result
*/
public int changePw(Member member);
/** 회원 탈퇴 DAO
* @param memberNo
* @return result
*/
public int secession(int memberNo);
}
외부에 파일을 따로 둠!
package edu.kh.project.myPage.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.member.model.dto.Member;
import edu.kh.project.myPage.model.service.MyPageService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@SessionAttributes({"loginMember"})
// 1) Model에 세팅된 값의 key와 {} 작성된 값이 일치하면 session scope로 이동
// 2) Session으로 올려둔 값을 해당 클래스에서 얻어와 사용 가능하게함
// -> @SessionAttribute(key)로 사용
@RequestMapping("/myPage")
@Controller
public class MyPageController {
@Autowired
private MyPageService service;
// 내 정보 페이지로 이동
@GetMapping("/info")
public String info() {
return "myPage/myPage-info";
}
// 프로필 페이지 이동
@GetMapping("/profile")
public String profile() {
return "myPage/myPage-profile";
}
// 비밀번호 변경 페이지 이동
@GetMapping("/changePw")
public String changePw() {
return "myPage/myPage-changePw";
}
// 탈퇴 페이지 이동
@GetMapping("/secession")
public String secession() {
return "myPage/myPage-secession";
}
// 회원 정보 수정
@PostMapping("/info")
public String updateInfo(@SessionAttribute("loginMember") Member loginMember,
Member updateMember,
String[] memberAddress,
RedirectAttributes ra) {
/*
* @SessionAttribute("loginMember") Member loginMember
* : Session에서 얻어온 "loginMember"에 해당하는 객체를
* 매개변수 Member loginMember에 저장
*
* Member updateMember
* : 수정할 닉네임, 전화번호 담긴 커맨드 객체
*
*
* String[] memberAddress
* : name="memberAddress"인 input 3개의 값(주소)
*
*
* RedirectAttributes ra : 리다이렉트 시 값 전달용 객체
*
* */
// 주소 하나로 합치자 (a^^^b^^^c)
if(updateMember.getMemberAddress().equals(",,")) {
updateMember.setMemberAddress(null);
}else {
// updateMember 에 주소문자열 세팅
String addr = String.join("^^^", memberAddress);
updateMember.setMemberAddress(addr);
}
// 로그인한 회원의 번호를 updateMember에 세팅
updateMember.setMemberNo( loginMember.getMemberNo() );
// DB 회원 정보 수정 (update) 서비스 호출
int result = service.updateInfo(updateMember);
String message = null;
// 결과값으로 성공
if(result > 0) {
// -> 성공 시 Session에 로그인된 회원 정보도 수정(동기화)
loginMember.setMemberNickname( updateMember.getMemberNickname() );
loginMember.setMemberTel( updateMember.getMemberTel() );
loginMember.setMemberAddress( updateMember.getMemberAddress() );
message = "회원 정보 수정 성공";
} else {
// 실패에 따른 처리
message = "회원 정보 수정 실패";
}
ra.addFlashAttribute("message", message);
return "redirect:info"; // 상대경로 (/myPage/info)
}
/* MultipartFile : input type="file"로 제출된 파일을 저장한 객체
*
* [ 제공하는 메서드 ]
* - transferTo() : 파일을 지정된 경로에 저장(메모리 -> HDD/SSD)
* - getOriginalFileName() : 파일 원본명
* - getSize() : 파일 크기
*
*
* */
// 프로필 이미지 수정
@PostMapping("/profile")
public String updateProfile(
@RequestParam("profileImage") MultipartFile profileImage // 업로드 파일
, @SessionAttribute("loginMember") Member loginMember
, RedirectAttributes ra // 리다이렉 시 메세지 전달
) throws Exception{
// 프로필 이미지 수정 서비스 호출
int result = service.updateProfile(profileImage, loginMember);
String message = null;
if(result > 0) message = "프로필 이미지가 변경되었습니다";
else message = "프로필 변경 실패";
ra.addFlashAttribute("message", message);
return "redirect:profile";
}
// 비밀번호 변경
@PostMapping("/changePw")
public String changePw(String currentPw, String newPw
,@SessionAttribute("loginMember") Member loginMember
,RedirectAttributes ra) {
// 로그인한 회원 번호(DB에서 어떤 회원을 조회, 수정하는지 알아야 되니까)
int memberNo = loginMember.getMemberNo();
// 비밀번호 변경 서비스 호출
int result = service.changePw(currentPw, newPw, memberNo);
String path = "redirect:";
String message = null;
if(result > 0) { // 변경 성공
message = "비밀번호가 변경 되었습니다.";
path += "info";
}else { // 변경 실패
message = "현재 비밀번호가 일치하지 않습니다.";
path += "changePw";
}
ra.addFlashAttribute("message", message);
return path;
}
// 회원 탈퇴
@PostMapping("/secession")
public String secession(String memberPw
,@SessionAttribute("loginMember") Member loginMember
,SessionStatus status
,HttpServletResponse resp
,RedirectAttributes ra) {
// String memberPw : 입력한 비밀번호
// SessionStatus status : 세션 관리 객체
// HttpServletResponse resp : 서버 -> 클라이언트 응답하는 방법 제공 객체
// RedirectAttributes ra : 리다이렉트 시 request로 값 전달하는 객체
// 1. 로그인한 회원의 회원 번호 얻어오기
// @SessionAttribute("loginMember") Member loginMember
int memberNo = loginMember.getMemberNo();
// 2. 회원 탈퇴 서비스 호출
// - 비밀번호가 일치하면 MEMBER_DEL_FL -> 'Y'로 바꾸고 1 반환
// - 비밀번호가 일치하지 않으면 -> 0 반환
int result = service.secession(memberPw, memberNo);
String path = "redirect:";
String message = null;
// 3. 탈퇴 성공 시
if(result > 0) {
// - message : 탈퇴 되었습니다
message = "탈퇴 되었습니다";
// - 메인 페이지로 리다이렉트
path += "/";
// - 로그아웃
status.setComplete();
// + 쿠키 삭제
Cookie cookie = new Cookie("saveId", "");
// 같은 쿠기가 이미 존재하면 덮어쓰기된다
cookie.setMaxAge(0); // 0초 생존 -> 삭제
cookie.setPath("/"); // 요청 시 쿠기가 첨부되는 경로
resp.addCookie(cookie); // 요청 객체를 통해서 클라이언트에게 전달
// -> 클라이언트 컴퓨터에 파일로 생성
}
// 4. 탈퇴 실패 시
else {
// - message : 현재 비밀번호가 일치하지 않습니다
message = "현재 비밀번호가 일치하지 않습니다";
// - 회원 탈퇴 페이지로 리다이렉트
path += "secession";
}
ra.addFlashAttribute("message",message);
return path;
}
}
package edu.kh.project.myPage.model.service;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.member.model.dto.Member;
public interface MyPageService {
/** 회원 정보 수정 서비스
* @param updateMember
* @return result
*/
int updateInfo(Member updateMember);
/** 프로필 이미지 수정 서비스
* @param profileImage
* @param webPath
* @param filePath
* @param loginMember
* @return result
*/
int updateProfile(MultipartFile profileImage, Member loginMember) throws Exception;
/** 비밀번호 변경 서비스
* @param currentPw
* @param newPw
* @param memberNo
* @return result
*/
int changePw(String currentPw, String newPw, int memberNo);
/** 회원 탈퇴 서비스
* @param memberPw
* @param memberNo
* @return result
*/
int secession(String memberPw, int memberNo);
}
package edu.kh.project.myPage.model.service;
import java.io.File;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.common.utility.Util;
import edu.kh.project.member.model.dto.Member;
import edu.kh.project.myPage.model.dao.MyPageMapper;
@Service
@PropertySource("classpath:/config.properties") // classpath -------> src/main/resource
public class MyPageServiceImpl implements MyPageService{
@Value("${my.member.webpath}") // @Value --------> 필드에서만 사용 가능
private String webPath;
@Value("${my.member.location}")
private String filePath;
@Autowired
private MyPageMapper mapper;
@Autowired // BCryptPasswordEncoder 의존성 주입(DI)
private BCryptPasswordEncoder bcrypt;
// 회원 정보 수정 서비스
@Transactional
@Override
public int updateInfo(Member updateMember) {
return mapper.updateInfo(updateMember);
}
// 프로필 이미지 수정 서비스
@Override
public int updateProfile(MultipartFile profileImage, Member loginMember) throws Exception {
// 프로필 이미지 변경 실패 대비
String temp = loginMember.getProfileImage(); // 기존에 가지고 있던 이전 이미지 저장
String rename = null; // 변경 이름 저장 변수
if(profileImage.getSize() > 0) { // 업로드된 이미지가 있을 경우
// 1) 파일 이름 변경
rename = Util.fileRename(profileImage.getOriginalFilename());
// 2) 바뀐 이름 loginMember에 세팅
loginMember.setProfileImage(webPath + rename);
} else { // 업로드된 이미지가 없는 경우 (x버튼)
loginMember.setProfileImage(null);
}
// 프로필 이미지 수정 DAO 메서드 호출
int result = mapper.updateProfileImage(loginMember);
if(result > 0) { // DB에 이미지 경로 업데이트 성공했다면
// 업로드된 새 이미지가 있을 경우
if(rename != null) {
// 메모리에 임시 저장되어있는 파일을 서버에 진짜로 저장하는 것
profileImage.transferTo(new File(filePath + rename));
}
} else { // 실패
// 이전 이미지로 프로필 다시 세팅
loginMember.setProfileImage(temp);
}
return result;
}
// 비밀번호 변경 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int changePw(String currentPw, String newPw, int memberNo) {
// 1. 현재 비밀번호, DB에 저장된 비밀번호 비교
// 1) 회원번호가 일치하는 MEMBER 테이블 행의 MEMBER_PW 조회
String encPw = mapper.selectEncPw(memberNo);
// 2) bcrypt.matches(평문, 암호문) -> 같으면 true -> 이 때 비번 수정
if(bcrypt.matches(currentPw, encPw)) {
Member member = new Member();
member.setMemberNo(memberNo);
member.setMemberPw(bcrypt.encode(newPw));
// 2. 비밀번호 변경(UPDATE DAO 호출) -> 결과 반환
return mapper.changePw( member );
}
// 3) 비밀번호가 일치하지 않으면 0 반환
return 0;
}
// 회원 탈퇴 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int secession(String memberPw, int memberNo) {
// 1. 회원 번호가 일치하는 회원의 비밀번호 조회
String encPw = mapper.selectEncPw(memberNo);
// 2.비밀번호가 일치하면
if(bcrypt.matches(memberPw, encPw)) {
// MEMBER_DEL_FL -> 'Y'로 바꾸고 1 반환
return mapper.secession(memberNo);
}
// 3. 비밀번호가 일치하지 않으면 -> 0 반환
return 0;
}
}
package edu.kh.project.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.servlet.MultipartConfigElement;
@Configuration
@PropertySource("classpath:/config.properties")
public class FileUploadConfig implements WebMvcConfigurer{
// WebMvcConfigurer : 스프링에서 웹 관련 요청/응답 모든 설정들을 할 수 있는 메서드를
// 기본 제공해주는 인터페이스
// 파일을 hdd(하드디스크)에 저장하기 전 임시로 가지고 있을 메모리 용량
@Value("${spring.servlet.multipart.file-size-threshold}")
private long fileSizethreshold;
// 파일 1개 크기 제한
@Value("${spring.servlet.multipart.max-file-size}")
private long maxFileSize;
// 요청당 파일 크기 제한
@Value("${spring.servlet.multipart.max-request-size}")
private long maxRequestSize;
@Bean // 개발자가 수동으로 Bean 등록(생성은 개발자, 관리는 Spring)
public MultipartConfigElement configElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setFileSizeThreshold(DataSize.ofBytes(fileSizethreshold)); // long형 값을 byte 크기로 나타냄
factory.setMaxFileSize(DataSize.ofBytes(maxFileSize));
factory.setMaxFileSize(DataSize.ofBytes(maxRequestSize));
return factory.createMultipartConfig();
}
@Bean
public MultipartResolver multipartResolver() {
// MultipartResolver : 파일은 파일로, 텍스트는 텍스트로 자동 구분
return new StandardServletMultipartResolver(); // 부트용 Resolver 사용!
}
// 웹에서 사용하는 자원을 다루는 방법을 설정
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// /images/ 로 시작되는 요청
String webPath = "/images/**"; // /images/ 로 시작되는 모든 요청
// 실제로 자원이 저장되어있는 로컬 경로 ------> C Drive / uploadImages
String resourcePath = "file:///C:/uploadImages/"; // 최상위 경로만 작성!
// /images/로 시작하는 요청이 오면, C:/uploadImages/ 와 연결
registry.addResourceHandler(webPath).addResourceLocations(resourcePath);
}
}
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Page</title>
<link rel="stylesheet" th:href="@{/css/myPage/myPage-style.css}">
</head>
<body>
<main>
<th:block th:replace="~{/common/header}"></th:block>
<!-- 마이페이지 - 내 정보 -->
<section class="myPage-content">
<!-- 사이드메뉴 include -->
<th:block th:replace="~{/myPage/sideMenu}"></th:block>
<!-- 오른쪽 마이페이지 주요 내용 부분 -->
<section class="myPage-main">
<h1 class="myPage-title">프로필</h1>
<span class="myPage-subject">프로필 이미지를 변경할 수 있습니다.</span>
<form th:action="@{profile}" th:object="${session.loginMember}" method="POST" name="myPageFrm" id="profileFrm" enctype="multipart/form-data">
<div class="profile-image-area">
<!-- 프로필 이미지가 없으면 기본 이미지 -->
<img th:unless="*{profileImage}" th:src="@{/images/user.png}" id="profileImage">
<!-- unless = false일 때 실행 -->
<!-- 프로필 이미지가 있으면 저장된 이미지 -->
<img th:if="*{profileImage}" th:src="*{profileImage}" id="profileImage">
</div>
<span id="deleteImage">x</span>
<div class="profile-btn-area">
<label for="imageInput">이미지 선택</label>
<input type="file" name="profileImage" id="imageInput" accept="image/*">
<button>변경하기</button>
</div>
<div class="myPage-row">
<label>이메일</label>
<span>로그인 회원 이메일</span>
</div>
<div class="myPage-row">
<label>가입일</label>
<span>로그인 회원 가입일</span>
</div>
</form>
</section>
</section>
</main>
<th:block th:replace="~{/common/footer}"></th:block>
<script th:src="@{/js/myPage/myPage.js}"></script>
</body>
</html>