회원정보 수정
# 회원정보 수정
내 정보변경 구현 로직
회원info 페이지에서 내정보를 먼저 내리겠다
- MemberController
@GetMapping("/member/info")
public String memberInfo(Model model, Principal principal){
String userId = principal.getName();
MemberDto detail = memberService.detail(userId);
model.addAttribute("detail", detail);
return "member/info";
}
- info.html
- password.html
신규 비밀번호와 신규 비밀번호 확인은 스크립트에서 체크
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(document).ready(function (){
$('#submitForm').on('submit', function (){
var $thisForm = $(this);
var newPassword = $thisForm.find('input[name=newPassword]').val();
var newRePassword = $thisForm.find('input[name=newRePassword]').val();
if(newPassword != newRePassword){
alert('신규 비밀번호와 확인이 같지 않습니다')
return false;
}
});
});
</script>
</head>
<body>
<h1>회원 정보</h1>
<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>
<div>
<hr/>
<a href="/member/info">회원정보 수정</a>
|
<a href="/member/password">비밀번호 변경</a>
|
<a href="/member/takecourse">내 수강 목록</a>
<hr/>
</div>
<div>
<form id="submitForm" method="post">
<div>
<input type="password" name="password" placeholder="현재 비밀번호 입력" required>
</div>
<div>
<input type="password" name="newPassword" placeholder="신규 비밀번호 입력" required>
</div>
<div>
<input type="password" name="newRePassword" placeholder="신규 확인 비밀번호 입력" required>
</div>
<div>
<button type="submit">비밀번호 변경</button>
</div>
</form>
</div>
</body>
</html>
- MemberController
@GetMapping("/member/password")
public String memberPassword(Model model, Principal principal){
String userId = principal.getName();
MemberDto detail = memberService.detail(userId);
model.addAttribute("detail", detail);
return "member/password";
}
@PostMapping("/member/password")
public String memberPasswordSubmit(
Model model, Principal principal, MemberRequestDto param){
String userId = principal.getName();
param.setUserId(userId);
ServiceResult result = memberService.updateMemberPassword(param);
if(!result.isResult()){
model.addAttribute("message", result.getMessage());
return "common/error";
}
return "redirect:/member/info";
}
- MemberRequestDto
@Data
@ToString
public class MemberRequestDto {
private String userId;
private String userName;
private String phone;
private String password;
// 추가
private String newPassword;
}
- MemberService
// 회원정보 페이지 내 비밀번호 변경 기능
ServiceResult updateMemberPassword(MemberRequestDto param);
- MemberServiceImpl
@Override
public ServiceResult updateMemberPassword(MemberRequestDto param) {
String userId = param.getUserId();
// 패스워드 일치확인
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()){
return new ServiceResult(false,"회원정보가 존재하지 않습니다");
}
Member member = optionalMember.get();
// BCrypt 안에 있는 체크기능능
if(!BCrypt.checkpw(param.getPassword(), member.getPassword())){
return new ServiceResult(false,"비밀번호가 일치하지 않습니다");
}
String encPassword = BCrypt.hashpw(param.getNewPassword(), BCrypt.gensalt());
member.setPassword(encPassword);
memberRepository.save(member);
return new ServiceResult(true);
}
# 회원정보 수정 - 정보수정
회원정보는 크게
화면에 보여주지 않는 정보들(패스워드, 이메일 인증키 등)
화면에만 보여주는 정보들(수정이 불가한 내용들, read only)
수정이 가능한 정보들
로 나뉜다
- info.html
회원정보 수정은 전화번호만 수정할 수 있게 구성
<body>
<h1>회원 정보</h1>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(function() {
$('#updateForm').on('submit', function() {
if (!confirm('회원정보를 수정하시겠습니까?')) {
return false;
}
});
});
</script>
<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>
<div>
<hr/>
<a href="/member/info">회원정보 수정</a>
|
<a href="/member/password">비밀번호 변경</a>
|
<a href="/member/takecourse">내 수강 목록</a>
<hr/>
</div>
<div>
<form id="updateForm" method="post">
<table>
<tbody>
<tr>
<th>아이디(이메일)</th>
<td>
<p th:text="${detail.userId}">아이디</p>
</td>
</tr>
<tr>
<th>이름</th>
<td>
<p th:text="${detail.userName}">아이디</p>
</td>
</tr>
<tr>
<th>전화번호</th>
<td>
// 서버로 전송되는 데이터에 기준은 name이기에
// 서버로 데이터를 보내 변경,삭제하는 경우에는 name을 dto와 맞춰 보내야한다
<input name="phone" type="text" th:value="${detail.phone}">
</td>
</tr>
<tr>
<th>가입일</th>
<td>
<p th:text="${detail.regDtText}">아이디</p>
</td>
</tr>
</tbody>
</table>
<div>
<button type="submit">수정</button>
</div>
</form>
</div>
</body>
- MemberController
MemberInput으로 회원정보를 보냄
@PostMapping("/member/info")
public String updateInfo(Model model, MemberRequestDto param, Principal principal){
String userId = principal.getName();
param.setUserId(userId);
ServiceResult result = memberService.updateMember(param);
if(!result.isResult()){
model.addAttribute("message", result.getMessage());
return "common/error";
}
return "redirect:/member/info";
}
- MemberService
// 회원정보 수정
ServiceResult updateMember(MemberRequestDto param);
- MemberServiceImpl
@Override
public ServiceResult updateMember(MemberRequestDto param) {
String userId = param.getUserId();
// 패스워드 일치확인
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()){
return new ServiceResult(false,"회원정보가 존재하지 않습니다");
}
Member member = optionalMember.get();
member.setPhone(param.getPhone());
memberRepository.save(member);
return new ServiceResult(true);
}
수정일 만들기
- MemberServiceImpl
member.setPhone(param.getPhone());
// 이부분 추가!
member.setUptDt(LocalDateTime.now());
memberRepository.save(member);
- detail.html
+ Member 엔티티에 uptDt추가
<tr>
<th>수정일</th>
<td>
<p th:text="${detail.uptDtText}">수정일</p>
</td>
</tr>
- MemberDto
데이터 교환을 위해 dto클래스에도 수정관한 부분 추가
public class MemberDto {
...
LocalDateTime regDt;
LocalDateTime uptDt;
...
public static MemberDto of(Member member){
return MemberDto.builder()
.userId(member.getUserId())
.userName(member.getUserName())
.phone(member.getPhone())
.regDt(member.getRegDt())
// 빌더에서도 추가를 해줘야 데이터가 변환된다
.uptDt(member.getUptDt())
.emailAuthYn(member.isEmailAuthYn())
.emailAuthDt(member.getEmailAuthDt())
.emailAuthKey(member.getEmailAuthKey())
.restPasswordKey(member.getResetPasswordKey())
.resetPasswordLimitDt(member.getResetPasswordLimitDt())
.adminYn(member.isAdminYn())
.userStatus(member.getUserStatus())
.build();
}
...
public String getUptDtText(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return uptDt != null ? uptDt.format(formatter) : "";
}
}
# 우편번호 API를 이용한 주소정보 수정 구현
- Member
주소관련 컬럼추가
private String zipcode;
private String addr;
private String addrDetail;
-MemberDto
private String zipcode;
private String addr;
private String addrDetail;
public static MemberDto of(Member member){
return MemberDto.builder()
.userId(member.getUserId())
.userName(member.getUserName())
.phone(member.getPhone())
.regDt(member.getRegDt())
.uptDt(member.getUptDt())
.emailAuthYn(member.isEmailAuthYn())
.emailAuthDt(member.getEmailAuthDt())
.emailAuthKey(member.getEmailAuthKey())
.restPasswordKey(member.getResetPasswordKey())
.resetPasswordLimitDt(member.getResetPasswordLimitDt())
.adminYn(member.isAdminYn())
.userStatus(member.getUserStatus())
.zipcode(member.getZipcode())
.addr(member.getAddr())
.addrDetail(member.getAddrDetail())
.build();
}
이제 데이터를 저장하는 부분 작성
- info.html
우편상세번호 외에는 readonly옵션을 줘서 변경이 안되게 하고 우편번호를 클릭해야만 변경이 가능하도록 설정
<div>
<form id="updateForm" method="post">
<table>
<tbody>
<tr>
<th>아이디(이메일)</th>
<td>
<p th:text="${detail.userId}">아이디</p>
</td>
</tr>
<tr>
<th>이름</th>
<td>
<p th:text="${detail.userName}">아이디</p>
</td>
</tr>
<tr>
<th>전화번호</th>
<td>
<input name="phone" type="text" th:value="${detail.phone}">
</td>
</tr>
<tr>
<th>주소</th>
<td>
<div>
<input type="text" id="zipcode" name="zipcode" th:value="${detail.zipcode}" readonly placeholder="우편번호 입력">
<button onclick="execDaumPostcode()" type="button">우편번호 입력</button>
</div>
<div>
<input id="addr" type="text" name="addr" th:value="${detail.addr}" readonly placeholder="주소 입력" />
<input id="addrDetail" type="text" name="addrDetail" th:value="${detail.addrDetail}" placeholder="주소 상세입력">
</div>
</td>
</tr>
<tr>
<th>가입일</th>
<td>
<p th:text="${detail.regDtText}">아이디</p>
</td>
</tr>
<tr>
<th>수정일</th>
<td>
<p th:text="${detail.uptDtText}">수정일</p>
</td>
</tr>
</tbody>
</table>
<div>
<button type="submit">수정</button>
</div>
</form>
</div>
다음 api 주소에서 가져온 스크립트 내용이며 실제 값이 들어갈 필드만 id명과 api 스크립트 내에있는 필드 이름과 같이 설정하면 된다
<!-- 다음 우편번호 api -->
<!-- iOS에서는 position:fixed 버그가 있음, 적용하는 사이트에 맞게 position:absolute 등을 이용하여 top,left값 조정 필요 -->
<div id="layer" style="display:none;position:fixed;overflow:hidden;z-index:1;-webkit-overflow-scrolling:touch;">
<img src="//t1.daumcdn.net/postcode/resource/images/close.png" id="btnCloseLayer" style="cursor:pointer;position:absolute;right:-3px;top:-3px;z-index:1" onclick="closeDaumPostcode()" alt="닫기 버튼">
</div>
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
// 우편번호 찾기 화면을 넣을 element
var element_layer = document.getElementById('layer');
function closeDaumPostcode() {
// iframe을 넣은 element를 안보이게 한다.
element_layer.style.display = 'none';
}
function execDaumPostcode() {
new daum.Postcode({
oncomplete: function(data) {
// 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
// 각 주소의 노출 규칙에 따라 주소를 조합한다.
// 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
var addr = ''; // 주소 변수
var extraAddr = ''; // 참고항목 변수
//사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
addr = data.roadAddress;
} else { // 사용자가 지번 주소를 선택했을 경우(J)
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("sample2_extraAddress").value = extraAddr;
} else {
// document.getElementById("sample2_extraAddress").value = '';
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
document.getElementById('zipcode').value = data.zonecode;
document.getElementById("addr").value = addr;
// 커서를 상세주소 필드로 이동한다.
document.getElementById("addrDetail").focus();
// iframe을 넣은 element를 안보이게 한다.
// (autoClose:false 기능을 이용한다면, 아래 코드를 제거해야 화면에서 사라지지 않는다.)
element_layer.style.display = 'none';
},
width : '100%',
height : '100%',
maxSuggestItems : 5
}).embed(element_layer);
// iframe을 넣은 element를 보이게 한다.
element_layer.style.display = 'block';
// iframe을 넣은 element의 위치를 화면의 가운데로 이동시킨다.
initLayerPosition();
}
// 브라우저의 크기 변경에 따라 레이어를 가운데로 이동시키고자 하실때에는
// resize이벤트나, orientationchange이벤트를 이용하여 값이 변경될때마다 아래 함수를 실행 시켜 주시거나,
// 직접 element_layer의 top,left값을 수정해 주시면 됩니다.
function initLayerPosition(){
var width = 300; //우편번호서비스가 들어갈 element의 width
var height = 400; //우편번호서비스가 들어갈 element의 height
var borderWidth = 5; //샘플에서 사용하는 border의 두께
// 위에서 선언한 값들을 실제 element에 넣는다.
element_layer.style.width = width + 'px';
element_layer.style.height = height + 'px';
element_layer.style.border = borderWidth + 'px solid';
// 실행되는 순간의 화면 너비와 높이 값을 가져와서 중앙에 뜰 수 있도록 위치를 계산한다.
element_layer.style.left = (((window.innerWidth || document.documentElement.clientWidth) - width)/2 - borderWidth) + 'px';
element_layer.style.top = (((window.innerHeight || document.documentElement.clientHeight) - height)/2 - borderWidth) + 'px';
}
</script>
- MemberServiceImpl
@Override
public ServiceResult updateMember(MemberRequestDto param) {
String userId = param.getUserId();
// 패스워드 일치확인
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()){
return new ServiceResult(false,"회원정보가 존재하지 않습니다");
}
Member member = optionalMember.get();
member.setPhone(param.getPhone());
member.setZipcode(param.getZipcode());
member.setAddr(param.getAddr());
member.setAddrDetail(param.getAddrDetail());
member.setUptDt(LocalDateTime.now());
memberRepository.save(member);
return new ServiceResult(true);
}
+) 트러블 슈팅
mysql Incorrect string value: '\xED\x85\x8C\xEC\x8A\xA4...' for column~ 오류 해결(한글 설정)
ALTER TABLE 테이블명 CONVERT TO CHARSET UTF8 을 삽입해주면 된다
# RestApi를 이용한 수강 신청취소
내 수강신청 목록 구현
- MemberController
강좌 목록을 가져오기위한 서비스 메서드 생성
@GetMapping("/member/takecourse")
public String memberTakeCourse(Model model, Principal principal){
String userId = principal.getName();
List<TakeCourseDto> list =takeCourseService.myCourse(userId);
model.addAttribute("list", list);
return "member/takecourse";
}
- TakeCourseService
// 내 수강내역목록
List<TakeCourseDto> myCourse(String userId);
- TakeCourseServiceImpl
@Override
public List<TakeCourseDto> myCourse(String userId) {
TakeCourseParam param = new TakeCourseParam();
param.setUserId(userId);
List<TakeCourseDto> list = takeCourseMapper.selectMyCourse(param);
return list;
}
- TakeCourseMapper
관리자 입장에서의 모든 리스트가 아닌 회원만의 리스트가 나오기 위한 쿼리가 필요
userId에 대한 수강신청 내역을 가져오겠다
<select id="selectMyCourse" resultType="com.zerobase.fastlms.course.dto.TakeCourseDto">
select tc.*
, c.subject
from take_course tc
join course c on tc.course_id = c.id
where tc.user_id = #{userId}
order by reg_dt desc
</select>
- takecourse.html
사용자 입장에서는 수강취소만 할 수 있도록 설정
번호는 타임리프의 index를 사용
// i를 설정하여 index값(번호)를 불러올 수 있다.
<tr th:each="x, i : ${list}">
<td th:text="${i.index + 1}">1</td>
<td>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>회원 정보</h1>
<style>
.list table {
width: 100%;
border-collapse: collapse;
}
.list table th, .list table td {
border:solid 1px #000;
}
p.nothing {
text-align: center;
padding: 100px;
}
.list .buttons {
position: relative;
padding: 10px 0;
}
.list .buttons a,.list .buttons button {
border-width: 0;
background-color: transparent;
text-decoration: underline;
font-size: 14px;
line-height: 20px;
height: 20px;
color: #000;
cursor: pointer;
}
</style>
<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>
<div>
<hr/>
<a href="/member/info">회원정보 수정</a>
|
<a href="/member/password">비밀번호 변경</a>
|
<a href="/member/takecourse">내 수강 목록</a>
<hr/>
</div>
<div class="list">
<table>
<thead>
<tr>
<th> NO </th>
<th>
등록일
</th>
<th>
강좌명
</th>
<th>
상태
</th>
<th>
비고
</th>
</tr>
</thead>
<tbody id="dataLIst">
<tr th:each="x, i : ${list}">
<td th:text="${i.index + 1}">1</td>
<td>
<p th:text="${x.regDtText}">2021.01.01</p>
</td>
<td>
<p th:href="'edit?id=' + ${x.id}" th:text="${x.subject}">강좌명</p>
</td>
<td>
<p th:if="${x.status eq 'REQ'}">수강신청</p>
<p th:if="${x.status eq 'COM'}">결제완료</p>
<p th:if="${x.status eq 'CANCEL'}">수강취소</p>
</td>
<td>
<div class="row-buttons" th:if="${x.status eq 'REQ'}">
<input type="hidden" name="id" th:value="${x.id}">
<button value="CANCEL" type="button">수강취소 처리</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
수강취소는 신청내역이 나와 동일한지 체크하고 내 정보와 동일할 때만 취소되도록 구현
사용자는 수강취소만 할수 있기에 COMPLETE는 관련없고 취소만 하도록 해야함
여기서는 ajax로 처리
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(document).ready(function() {
// 여기서의 button은 수강완료가 될수도 있고 수강취소가 될 수도 있다
$('.row-buttons button').on('click', function (){
// this = row-buttons, 상위 div로 올라가서 input의 name이 id인것을 find
var id = $(this).val();
var msg = '수강 취소 하시겠습니까';
if(!confirm(msg)){
return false;
}
var url = '/api/member/course/cancel.api';
var parameter = {
takeCourseId: id
};
axios.post(url, parameter).then(function(response) {
console.log(response);
console.log(response.data);
response.data = response.data || {};
response.data.header = response.data.header || {};
if (!response.data.header.result) {
alert(response.data.header.message);
return false;
}
//정상적일때
alert(' 강좌가 정상적으로 취소되었습니다. ');
location.reload();
}).catch(function(err) {
console.log(err);
});
return false;
});
});
</script>
- TakeCourseInput
클라이언트에서 넘겨주는 아이디가 takeCourseId이기 때문에 서버에서 받는 아이디도 같이 맞춰준다
public class TakeCourseInput {
// courseId
long courseId;
long takeCourseId;
String userId;
}
- ApiMemberController
내 강좌인지 체크해야 하는데 어차피 페이지에는 내 강좌밖에 없는데? api 호출은 조작이 가능하기 때문에 변경하기 전에는 서버에서 체크가 이루어져야함
ajax로 값을 받으니 ResponseEntity로 리턴
@PostMapping("/api/member/course/cancel.api")
public ResponseEntity<?> courseCancel(Model model,
@RequestBody TakeCourseInput param,
Principal principal){
String userId = principal.getName();
// 내 강좌인지 확인
TakeCourseDto detail = takeCourseService.detail(param.getTakeCourseId());
if (detail == null){
ResponseResult responseResult = new ResponseResult(false, "수강진청 정보가없습니다");
return ResponseEntity.ok().body(responseResult);
}
// 내 수강신청 정보가 아닌경우
if (userId == null || !userId.equals(detail.getUserId())){
ResponseResult responseResult = new ResponseResult(false, "본인의 수강신청 정보만 취소가능합니다");
return ResponseEntity.ok().body(responseResult);
}
// 위를 모두 통과했다면 취소메서드 실행
- TakeCourseService
단건조회하는 detail 필요
// 수강상제정보
TakeCourseDto list(long id);
- TakeCourseServiceImpl
@Override
public TakeCourseDto detail(long id) {
Optional<TakeCourse> optionalTakeCourse = takeCourseRepository.findById(id);
if(optionalTakeCourse.isPresent()){
return TakeCourseDto.of(optionalTakeCourse.get());
}
return null;
}
- TakeCourseDto
public static TakeCourseDto of(TakeCourse t) {
return TakeCourseDto.builder()
.id(t.getId())
.courseId(t.getCourseId())
.userId(t.getUserId())
.payPrice(t.getPayPrice())
.status(t.getStatus())
.regDt(t.getRegDt())
.build();
}
취소 메서드 만들기
- TakeCourseService
// 수강신청취소
ServiceResult cancel(long id);
- TakeCourseServiceImpl
@Override
public ServiceResult cancel(long id) {
Optional<TakeCourse> optionalTakeCourse = takeCourseRepository.findById(id);
if(!optionalTakeCourse.isPresent()){
return new ServiceResult(false,"수강정보가 존재하지 않습니다");
}
TakeCourse takeCourse = optionalTakeCourse.get();
takeCourse.setStatus(TakeCourseCode.STATUS_CANCEL);
takeCourseRepository.save(takeCourse);
return new ServiceResult();
}
- ApiMemberController
@PostMapping("/api/member/course/cancel.api")
public ResponseEntity<?> courseCancel(Model model,
@RequestBody TakeCourseInput param,
Principal principal){
String userId = principal.getName();
// 내 강좌인지 확인
TakeCourseDto detail = takeCourseService.detail(param.getTakeCourseId());
if (detail == null){
ResponseResult responseResult = new ResponseResult(false, "수강진청 정보가없습니다");
return ResponseEntity.ok().body(responseResult);
}
// 내 수강신청 정보가 아닌경우
if (userId == null || !userId.equals(detail.getUserId())){
ResponseResult responseResult = new ResponseResult(false, "본인의 수강신청 정보만 취소가능합니다");
return ResponseEntity.ok().body(responseResult);
}
// 위를 모두 통과했다면 취소메서드 실행
ServiceResult result =takeCourseService.cancel(param.getTakeCourseId());
if(!result.isResult()){
ResponseResult responseResult = new ResponseResult(false, result.getMessage());
return ResponseEntity.ok().body(responseResult);
}
ResponseResult responseResult = new ResponseResult(true);
return ResponseEntity.ok().body(responseResult);
}
}
+) 트러블 슈팅
모든 로직이 완성된 후 페이지에서 수강신청을 누르면 null 을 반환하고 f5를 눌러야 수강신청취소 처리가 되는 문제가 발생하였다
어찌되었든 디비에도 취소값이 들어가고 클라이언트단에서도 f5를 눌러도 취소가 되니 로직은 문제가 없다는 건데 계속 오류가 나기에 찾아보니 위의 사진을 보면 데이터가 정상적으로 들어왔음에도 false를 반환하는 것을 볼 수 있다
ServiceResult를 가보니
@Data
@NoArgsConstructor
public class ServiceResult {
boolean result;
String message;
public ServiceResult(boolean result, String message) {
this.result = result;
this.message = message;
}
public ServiceResult(boolean result) {
this.result = result;
}
}
기본생성자를 NoArg로 받아서 디폴트값이 false로 들어가서 그랬나보다
@Data
public class ServiceResult {
boolean result;
String message;
public ServiceResult() {
result = true;
}
public ServiceResult(boolean result, String message) {
this.result = result;
this.message = message;
}
public ServiceResult(boolean result) {
this.result = result;
}
}
그래서 기본생성자를 트루로 바꾸어주니 데이터가 정상적으로 들어갔다
# 회원탈퇴
회원탈퇴는 삭제가 아닌 RESET형태로 전환
회원이 탈퇴하더라도 회원이 페이지에서 행했던 모든 활동을 삭제하는 것이 아니다
- withdraw.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>회원 탈퇴</h1>
<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>
<div>
<hr/>
<a href="/member/info">회원정보 수정</a>
|
<a href="/member/password">비밀번호 변경</a>
|
<a href="/member/takecourse">내 수강 목록</a>
<hr/>
</div>
<div>
<p>
회원탈퇴를 하면
서비스를 더 이상 이용하실 수 없습니다
<br/>
회원 탈퇴를 진행하시겠습니까?
</p>
<form id="submitForm" method="post">
<div>
<input type="password" name="password" placeholder="현재 비밀번호 입력" required>
</div>
<div>
<button type="submit">회원 탈퇴하기</button>
</div>
</form>
</div>
</body>
</html>
- MemberController
password를 받기 위해 MemberInput에서 받아온다 , withdraw.html에서 넘겨준 name과 input에서 name이 같아야 데이터가 송신된다
@GetMapping("/member/withdraw")
public String memberWithdraw(Model model){
return "member/withdraw";
}
@PostMapping("/member/withdraw")
public String memberWithdrawSubmit(Model model, Principal principal, MemberRequestDto param){
String userId = principal.getName();
// 통과한다면 회원탈퇴 진행
ServiceResult result = memberService.withdraw(userId, param.getPassword());
if(!result.isResult()){
model.addAttribute("message",result.getMessage());
return "common/error";
}
return "redirect:/member/logout";
}
}
- PasswordUtils
public class PasswordUtil {
public static boolean equals(String plaintext, String hashed){
if(plaintext == null || plaintext.length() < 1){
return false;
}
if (hashed == null || hashed.length() < 1) {
return false;
}
return BCrypt.checkpw(plaintext, hashed);
}
// 해쉬함수화
public static String encPassword(String plaintext){
if(plaintext == null || plaintext.length() < 1){
return "";
}
return BCrypt.hashpw(plaintext, BCrypt.gensalt());
}
}
+) 트러블 : 패스워드를 해쉬화할때 파라미터값이 null 일때만 false를 반환해야하고 아닐때는 true인데
처음에 != 로 설정해서 패스워드가 암호화되지않고 그대로 스트링값으로 넘어와서 회원탈퇴가 되지않았음
만들어진 유틸을 멤버서비스에 적용한다(리팩토링 개념이라 생각하면 된다)
기존코드
@Override
public ServiceResult updateMemberPassword(MemberRequestDto param) {
...
if(!BCrypt.equals(param.getPassword(), member.getPassword())){
return new ServiceResult(false,"비밀번호가 일치하지 않습니다");
}
String encPassword = BCrypt.hashpw(param.getNewPassword(), BCrypt.gensalt());
member.setPassword(encPassword);
memberRepository.save(member);
return new ServiceResult(true);
}
유틸을 적용한 코드
@Override
public ServiceResult updateMemberPassword(MemberRequestDto param) {
...
if(!PasswordUtil.equals(param.getPassword(), member.getPassword())){
return new ServiceResult(false,"비밀번호가 일치하지 않습니다");
}
String encPassword = PasswordUtil.encPassword(param.getNewPassword());
member.setPassword(encPassword);
memberRepository.save(member);
return new ServiceResult(true);
}
- MemberService
// 회원탈퇴 로직
ServiceResult withdraw(String userId, String password);
- MemberServiceImpl
관리적차원에서 takeCourse는 남기고 회원쪽의 정보만 지우려고 함
@Override
public ServiceResult withdraw(String userId) {
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()){
return new ServiceResult(false,"회원정보가 없습니다");
}
Member member = optionalMember.get();
member.setUserName("삭제회원");
member.setPhone("");
member.setPassword("");
member.setRegDt(null);
member.setUptDt(null);
member.setEmailAuthYn(false);
member.setEmailAuthKey("");
member.setResetPasswordKey("");
member.setResetPasswordLimitDt(null);
member.setUserStatus(MemberCode.MEMBER_STATUS_WITHDRAW);
member.setZipcode("");
member.setAddr("");
member.setAddrDetail("");
memberRepository.save(member);
return new ServiceResult();
}
이 로직을 다 생성하고 나면 비밀번호가 일치하지 않는다는 오류가 발생
이는 Controller에서 detail 메서드를 사용할 때 MemberDto의 of메서드에서 패스워드 값을 내려주지 않았기 때문이다
이를 해결하기 위해서 컨트롤러에서 비밀번호를 체크하는 로직을 서비스단으로 옮겨서 진행
- MemberController
@PostMapping("/member/withdraw")
public String memberWithdrawSubmit(Model model, Principal principal, MemberRequestDto param){
String userId = principal.getName();
// 통과한다면 회원탈퇴 진행
ServiceResult result = memberService.withdraw(userId, param.getPassword());
if(!result.isResult()){
model.addAttribute("message",result.getMessage());
return "common/error";
}
return "redirect:/member/logout";
}
}
- MemberServiceImpl
패스워드를 파라미터로 넘어온 값과 멤버에서 넘어온 패스워드를 비교
@Override
public ServiceResult withdraw(String userId, String password) {
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()){
return new ServiceResult(false,"회원정보가 없습니다");
}
Member member = optionalMember.get();
// 비밀번호가 다르다면 회월탈퇴 x
// 원래 컨트롤러에 있던 코드를 서비스단으로 옮김
if(!PasswordUtil.equals(password, member.getPassword())){
return new ServiceResult(false, "비밀번호가 일치하지 않습니다");
}
member.setUserName("삭제회원");
member.setPhone("");
member.setPassword("");
member.setRegDt(null);
member.setUptDt(null);
member.setEmailAuthYn(false);
member.setEmailAuthKey("");
member.setResetPasswordKey("");
member.setResetPasswordLimitDt(null);
member.setUserStatus(MemberCode.MEMBER_STATUS_WITHDRAW);
member.setZipcode("");
member.setAddr("");
member.setAddrDetail("");
memberRepository.save(member);
return new ServiceResult();
}
로직구현을 마치면 회원에 대한 정보는 리셋되어 볼 수 없지만 수강에 대한 내역은 조회할 수 있다
- MemberServiceImpl
탈퇴된 회원에 대한 로그인 처리
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
...
if(Member.MEMBER_STATUS_WITHDRAW.equals(member.getUserStatus())){
throw new MemberStopUserlAuthException("탈퇴된 회원입니다");
}
...
return new User(member.getUserId(), member.getPassword(),grantedAuthorities);
}
}