16.1 회원 정보 수정 구현
Ex) 마이페이지 내 정보 수정
- 멤버 컨트롤러 - > 시쿠리티에 대한 인터페이스를 매개변수로 입력하면 스프링이 주입해준다.
<hide/>
@GetMapping("/member/info")
public String memberInfo(Model model, Principal principal){
String userId = principal.getName();
MemberDto detail = memberService.detail(userId);
model.addAttribute("detail", detail); // 그럼 이제 info에서는 detail에 대해 조회가능
return "member/info";
}
Note) 실행 결과
16.2 회원 비밀 번호 변경
- 비밀번호 변경 / 회원 정보 수정 페이지 / 수강 정보 확인
Ex)
- 멤버인포
- takecourse
- 컨트롤러 - password, takecourse
- password
-> 두 개의 비밀번호가 같은지 확인한다.
-> 다르게 입력한 경우
- 멤버 서비스
<hide/>
/**
* 회원 정보 페이지 비밀번호 변경
*/
boolean updateMemberPassword(MemberInput parameter);
- 컨트롤러 post 구현하기
<hide/>
@PostMapping("/member/password")
public String memberPasswordSubmit(Model model,
MemberInput parameter,
Principal principal){
String userId = principal.getName();
parameter.setUserId(userId);
ServiceResult result = memberService.updateMemberPassword(parameter);
if(!result.isResult()){
model.addAttribute("message", result.getMessage());
return "common/error";
}
return "redirect:/member/info";
}
- Impl
-> 두 비밀번호를 확인한다.
<hide/>
@Override
public ServiceResult updateMemberPassword(MemberInput parameter) {
String userId = parameter.getUserId();
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()) {
return new ServiceResult(false, "회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
if(!BCrypt.checkpw(parameter.getPassword(), member.getPassword())){
return new ServiceResult(false, "비밀번호가 일치하지 않습니다.");
}
String encPassword = BCrypt.hashpw(parameter.getNewPassword(), BCrypt.gensalt());
member.setPassword(encPassword);
memberRepository.save(member);
return new ServiceResult(true);
}
Note) 실행 결과 - 정상적으로 비밀번호가 변경된다.
16.3 회원 정보 수정
- 화면에 보여줄 정보 / 보여주지 않을 정보/ 수정 가능한 정보들을 각각 구분해야한다.
Ex) 회원 정보 수정하기
- 데이터 구분
-> 수정 불가능: 이름
-> 수정 가능: 전화번호
-> 정보성 데이터: 가입일
- 멤버 DTO 클래스에 날짜 형태를 바꿔주는 getRegDtText()를 넣어주면
-> 컨트롤러에서 model에 Dto 형태의 데이터를 "detail"이라는 이름으로 저장하고 그러면 info.html에서 detail과 메서드 getRegDtText()를 사용 가능하다.
<hide/>
public String getRegDtText(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss");
return regDt != null ? regDt.format(formatter) : "";
}
- 컨트롤러
<hide/>
@GetMapping("/member/info")
public String memberInfo(Model model, Principal principal){
String userId = principal.getName();
MemberDto detail = memberService.detail(userId);
model.addAttribute("detail", detail); // 그럼 이제 info에서는 detail에 대해 조회가능
return "member/info";
}
- info 페이지
<hide/>
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 정보</title>
<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>
</head>
<body>
<!-- <div th:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
<h1>회원 정보</h1>
<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>
<input name="phone" type="text" th:value="${detail.phone}"/>
</td>
</tr>
<tr>
<th>가입일</th>
<td>
<p th:text="${detail.getRegDtText}">가입일</p>
</td>
</tr>
<tr>
<th>회원정보 수정일</th>
<td>
<p th:text="${detail.getUdtDtText}">수정일</p>
</td>
</tr>
</tbody>
</table>
<div>
<button type="submit">수정</button>
</div>
</form>
</div>
</body>
</html>
- 컨트롤러 삭제 구현
<hide/>
@PostMapping("/member/info")
public String memberInfoSubmit(Model model,
MemberInput parameter,
Principal principal){
String userId = principal.getName();
parameter.setUserId(userId);
ServiceResult result = memberService.updateMember(parameter);
if(!result.isResult()){
model.addAttribute("message", result.getMessage()); // 그럼 이제 info에서는 detail에 대해 조회가능
return "common/error";
}
return "redirect:/member/info";
}
- updateMember()
<hide/>
@Override
public ServiceResult updateMember(MemberInput parameter) {
String userId = parameter.getUserId();
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()) {
return new ServiceResult(false, "회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
member.setPhone(parameter.getPhone());
memberRepository.save(member);
return new ServiceResult(true);
}
Note) 실행 결과 - 정상으로 변경된다.
16.4 다음 우편번호 API를 이용한 주소 정보 수정
Ex) 회원 정보에 주소 추가하기
- 주소 관련해서 멤버, 멤버 dto, MemberInput에 3가지 변수를 모두 추가한다.
- 멤버 인포 페이지
<hide/>
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 정보</title>
<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>
</head>
<body>
<!-- <div th:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
<h1>회원 정보</h1>
<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>
<input name="phone" type="text" th:value="${detail.phone}"/>
</td>
</tr>
<tr>
<th>주소</th>
<td>
<div>
<input type="text" name="zipcode" th:value="${detail.zipcode}" _readonly placeholder="우편번호 입력"/>
<button type="button">우편 번호 찾기</button>
</div>
<div>
<input type="text" name="addr" th:value="${detail.addr}" _readonly placeholder="주소 입력">
<input type="text" name="addrDetail" th:value="${detail.addrDetail}" placeholder="상세 주소 입력">
</div>
</td>
</tr>
<tr>
<th>가입일</th>
<td>
<p th:text="${detail.getRegDtText}">가입일</p>
</td>
</tr>
<tr>
<th>회원정보 수정일</th>
<td>
<p th:text="${detail.getUdtDtText}">수정일</p>
</td>
</tr>
</tbody>
</table>
<div>
<button type="submit">수정</button>
</div>
</form>
</div>
</body>
</html>
- 임플 - update
<hide/>
@Override
public ServiceResult updateMember(MemberInput parameter) {
String userId = parameter.getUserId();
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()) {
return new ServiceResult(false, "회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
member.setPhone(parameter.getPhone());
member.setUdtDt(LocalDateTime.now());
member.setZipcode(parameter.getZipcode());
member.setAddr(parameter.getAddr());
member.setAddrDetail(parameter.getAddrDetail());
memberRepository.save(member);
return new ServiceResult(true);
}
Note) 실행 결과
Ex) 우편번호 찾기
- 서버에 띄우는 백엔드 영역이라면 우편번호 찾기 띄우는 부분은 클라이언트 영역이다.
- 사이트 접속 https://postcode.map.daum.net/guide#sample
-> 팝업의 경우 모바일에서 문제가 될 수 있으니 iframe 이용해서 띄운다.
-> (예제 코드) iframe을 이용하여 레이어 띄우기
<hide/>
<!-- 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 sample2_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('sample2_postcode').value = data.zonecode;
document.getElementById("sample2_address").value = addr;
// 커서를 상세주소 필드로 이동한다.
document.getElementById("sample2_detailAddress").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>
- 예제 코드를 변수이름만 바꿔서 info 페이지에 넣는다.
<hide/>
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 정보</title>
<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>
</head>
<body>
<!-- <div th:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
<h1>회원 정보</h1>
<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>
<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 type="text" id="addr" name="addr" th:value="${detail.addr}" _readonly placeholder="주소 입력">
<input type="text" id="addrDetail" name="addrDetail" th:value="${detail.addrDetail}" placeholder="상세 주소 입력">
</div>
</td>
</tr>
<tr>
<th>가입일</th>
<td>
<p th:text="${detail.getRegDtText}">가입일</p>
</td>
</tr>
<tr>
<th>회원정보 수정일</th>
<td>
<p th:text="${detail.getUdtDtText}">수정일</p>
</td>
</tr>
</tbody>
</table>
<div>
<button type="submit">수정</button>
</div>
</form>
</div>
<!--<다음 우편번호>-->
<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>
</body>
</html>
Note) 실행 결과
16.5 REST API를 이용한 수강 신청 취소
cf) REST API(Representational State Transfer Application Programming Interface) 란 무엇인가?
- REST(Representational State Transfer): 자원을 이름으로 구분하여 해당 자원의 상태(정보)를 주고 받는 모든 것을 의미한다.
- API(Application Programming Interface): 데이터와 기능의 집합을 제공하여 컴퓨터 프로그램 간 상호 작용을 촉진하며 서로 정보 교환을 가능하도록한다.
- REST API: REST기반으로 서비스 API를 구현한 것이다.
출처 https://gmlwjd9405.github.io/2018/09/21/rest-and-restful.html
Ex) 내 수강 목록 확인하기
- TakeCourse 매퍼에 추가한 selectListMyCourse메서드를 xml 파일에서 그대로 사용한다.
<hide/>
package com.zerobase.fastlms.course.mapper;
import com.zerobase.fastlms.course.dto.CourseDto;
import com.zerobase.fastlms.course.dto.TakeCourseDto;
import com.zerobase.fastlms.course.entity.TakeCourse;
import com.zerobase.fastlms.course.model.CourseParam;
import com.zerobase.fastlms.course.model.TakeCourseParam;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface TakeCourseMapper {
long selectListCount(TakeCourseParam parameter);
List<TakeCourseDto> selectList(TakeCourseParam parameter);
List<TakeCourseDto> selectListMyCourse(TakeCourseParam parameter);
}
- TakeCourse 매퍼.xml
-> 그런데 왜 resultType은 list가 아니고 Dto일까?
<hide/>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zerobase.fastlms.course.mapper.TakeCourseMapper">
<sql id="selectListWhere">
</sql>
<select id="selectListCount"
resultType="long">
SELECT COUNT(*)
FROM take_course
WHERE 1 = 1
<include refid="selectListWhere"/>
</select>
<select id="selectList"
resultType="com.zerobase.fastlms.course.dto.TakeCourseDto">
SELECT tc.*
, c.subject
, m.user_name
, m.phone
FROM take_course tc
JOIN course c ON tc.course_id = c.id
JOIN member m ON tc.user_id = m.user_id
WHERE 1 = 1
<include refid="selectListWhere"/>
ORDER BY reg_dt DESC
LIMIT #{pageStart}, #{pageEnd}
</select>
<!-- userId에 해당하는 수강 신청 정보를 가져온다. -->
<select id="selectListMyCourse"
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}
<include refid="selectListWhere"/>
ORDER BY reg_dt DESC
</select>
</mapper>
- TakeCourse 서비스 임플 - myCourse()
<hide/>
@Override
public List<TakeCourseDto> myCourse(String userId) {
TakeCourseParam parameter = new TakeCourseParam();
parameter.setUserId(userId);
List<TakeCourseDto> list = takeCourseMapper.selectListMyCourse(parameter);
return list;
}
- 컨트롤러 내용 추가
<hide/>
@GetMapping("/member/takecourse")
public String memberTakeCourse(Model model, Principal principal){
String userId = principal.getName();
List<TakeCourseDto> list = takeCourseService.myCourse(userId);
model.addAttribute("list", list); // 그럼 이제 info에서는 detail에 대해 조회가능
return "member/takecourse";
}
- takecourse 페이지 내용 추가
<hide/>
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 정보</title>
</head>
<body>
<!-- <div th:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
<h1>회원 정보</h1>
<div>
<hr/>
<a href="/member/info">회원 정보 수정</a>
|
<a href="/member/password">비밀번호 변경</a>
|
<a href="/member/takecourse">내 수강 목록</a>
<hr/>
</div>
<div>
<table>
<thead>
<tr>
<th>NO</th>
<th>등록일</th>
<th>강좌명</th>
<th>상태</th>
<th>비고</th>
</tr>
</thead>
<tbody id ="dataList">
<tr th:each="x : ${list}">
<td th:text="${x.seq}">1</td>
<td>
<p th:text="${x.regDtText}">2022.01.01</p>
</td>
<td>
<p th:text="${x.subject}">강좌명</p>
</td>
<td>
<p th:if="${x.status eq 'REQ'}">수강신청</p>
<p th:if="${x.status eq 'COMPLETE'}">결제완료</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="COMPLETE" type="button">결제완료 처리</button>
<button value="CANCEL" type="button">수강취소 처리</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
Note) 실행 결과
Ex) 수강 신청 목록의 번호 붙이기
- 현 상태
- ThymeLeaf의 인덱스를 이용한다.
-> 예제 코드를 참고한다.
<hide/>
<tr th:each="fInClient,index : ${factory.getFactoryClientList()}">
<td> <input type="text" class="form-control" id="client_name_0" th:value="${fInClient.getName()}"> </td>
<td>
<th:block th:if="${index.index} > 0">
<button type='button' class='btn btn-danger'> <i class='fa fa-minus'></i> </button>
</th:block>
</td>
<tr>
-> takecourse 페이지 - index + 1해줘야 1부터 나온다.
<hide/>
<tr th:each="x, i : ${list}">
<td th:text="${i.index + 1}">1</td>
=생략=
Note) 실행 결과
cf) Ajax(Asynchronous JavaScript and XML, 에이작스)란?
- Ajax란 비동기 방식(서버에 신호를 보냈을 때 응답 상태와 상관없이 다음 동작을 수행할 수 있음)의 Javascript와 XML을 가리킨다.
Ex) 수강 취소는 본인 인증을 거치고 나서 취소하도록 한다.
- 관리자 takecourse에서 Ajax로 처리한 부분을 재사용한다.
- 관리자 쪽에서는 결제 완료/ 수강취소 모두 가능하지만, 프론트 쪽에서는 단순히 수강 취소만 가능해야한다. (보안 이슈), 결제 완료 버튼 삭제
- takecourse.html
-> api를 하나 만들어서 호출하면 수강 신청이 취소되도록 한다. take_course의 courseId를 매개변수로 보낸다.
<hide/>
<div class="row-buttons" th:if="${x.status eq 'REQ'}">
<button type="button" th:value="${x.id}">수강취소 처리</button>
</div>
- 지금 상태
-> 여기서 기획 완성반 취소 버튼 누르면 기획 완성반의 courseId 번호를 확인 가능
- 앞으로 위의 API를 구현하면 된다.
- 컨트롤러 만든다.
-> API이기 때문에 @RestController
-> API호출하는 것은 조작이 가능하므로 변경 전에 반드시 서버에서 체크해야한다.
- detail() 구현
<hide/>
@Override
public TakeCourseDto detail(long id) {
Optional<TakeCourse> optionalTakeCourse = takeCourseRepository.findById(id);
if(optionalTakeCourse.isPresent()){
return TakeCourseDto.of(optionalTakeCourse.get());
}
return null;
}
- Dto에 of()
<hide/>
public static TakeCourseDto of(TakeCourseDto takeCourseDto) {
return TakeCourseDto.builder()
.id(takeCourseDto.getId())
.courseId(takeCourseDto.getCourseId())
.userId(takeCourseDto.getUserId())
.payPrice(takeCourseDto.getPayPrice())
.status(takeCourseDto.getStatus())
.regDt(takeCourseDto.getRegDt())
.build();
}
- 컨트롤러
-> 당연히 본인 강의만 뜰텐데 왜 굳이 if문을 추가할까?
- 취소 매서드 cancel(long id)
<hide/>
@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);
return new ServiceResult();
}
- Api Course컨트롤러
<hide/>
package com.zerobase.fastlms.course.controller;
import com.zerobase.fastlms.admin.service.CategoryService;
import com.zerobase.fastlms.common.model.ResponseResult;
import com.zerobase.fastlms.course.model.ServiceResult;
import com.zerobase.fastlms.course.model.TakeCourseInput;
import com.zerobase.fastlms.course.service.CourseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequiredArgsConstructor
public class ApiCourseController extends BaseController{
private final CourseService courseService;
private final CategoryService categoryService;
@PostMapping("api/course/req.api")
public ResponseEntity<?> courseReq(Model model,
@RequestBody TakeCourseInput parameter,
Principal principal){
parameter.setUserId(principal.getName());
ServiceResult result = courseService.req(parameter);
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); // 성공
}
}
- Api Member 컨트롤러
<hide/>
package com.zerobase.fastlms.member.controller;
import com.zerobase.fastlms.common.model.ResponseResult;
import com.zerobase.fastlms.course.dto.TakeCourseDto;
import com.zerobase.fastlms.course.model.ServiceResult;
import com.zerobase.fastlms.course.model.TakeCourseInput;
import com.zerobase.fastlms.course.service.TakeCourseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.security.Principal;
@Controller
@RequiredArgsConstructor
public class ApiMemberController {
private final TakeCourseService takeCourseService;
@PostMapping("api/member/course/cancel.api")
public ResponseEntity<?> cancelCourse(Model model, // principal
@RequestBody TakeCourseInput parameter,
Principal principal){
String userId = principal.getName();
// 내 강좌인지 확인해야 취소 권한이 있다.
TakeCourseDto detail = takeCourseService.detail(parameter.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(parameter.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);
}
}
Note) 실행 결과
-> 취소 전 화면
-> 수강 취소 후 화면
16.6 회원 탈퇴
- userId만 살려두고 나머지 정보는 지운다. 새로 가입하면 새로운 데이터 저장
- 회원 테이블 정보를 삭제하는 게 아니라 reset
- 관리적인 차원에서 take_course 정보는 삭제하지 않아도 된다.
Ex) 회원 탈퇴 구현하기
- info
<hide/>
<div>
<button type="submit">수정</button>
<a href ="/member/withdraw">회원 탈퇴</a>
</div>
- withdraw 페이지 만든다.
-> 비밀번호를 체크하고 탈퇴하도록 한다.
<hide/>
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 정보</title>
</head>
<body>
<!-- <div th:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
<h1>회원 탈퇴</h1>
<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>
</div>
</body>
</html>
- 컨트롤러 - GET & POST 구성
- PasswordUtils 클래스를 만든다.
<hide/>
package com.zerobase.fastlms.util;
import org.springframework.security.crypto.bcrypt.BCrypt;
public class PasswordUtils {
public static boolean equals(String plaintext, String hashed){
if(plaintext == null || plaintext.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());
}
}
- Impl클래스 - updateMemberPassword()
<hide/>
@Override
public ServiceResult updateMemberPassword(MemberInput parameter) {
String userId = parameter.getUserId();
Optional<Member> optionalMember = memberRepository.findById(userId);
if(!optionalMember.isPresent()) {
return new ServiceResult(false, "회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
if(PasswordUtils.equals(parameter.getPassword(), member.getPassword())){
return new ServiceResult(false, "비밀번호가 일치하지 않습니다.");
}
String encPassword = PasswordUtils.encPassword(parameter.getNewPassword());
member.setPassword(encPassword);
memberRepository.save(member);
return new ServiceResult(true);
}
- 컨트롤러
<hide/>
@GetMapping("/member/withdraw")
public String memberWithdraw(Model model){
return "member/withdraw";
}
@PostMapping("/member/withdraw")
public String memberWithdrawSubmit(Model model,
MemberInput parameter,
Principal principal){
String userId = principal.getName();
parameter.setUserId(userId);
ServiceResult result = memberService.withdraw(userId, parameter.getPassword()); ///????????????????????????????????????????????????
if(!result.isResult()){
model.addAttribute("message", result.getMessage());
return "common/error";
}
// 로그아웃을 시킨다.
return "redirect:/member/logout";
}
- Impl에 withdraw() 구현 - 이메일 빼고 모두 지운다.
<hide/>
@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();
if(!PasswordUtils.equals(password, member.getPassword())) { // 탈퇴 불가능 - 자리 바꾸니까 정상 탈퇴
return new ServiceResult(false, "비밀번호가 일치하지 않습니다.");
}
member.setUserName("삭제 회원");
member.setPhone("");
member.setPassword("");
member.setRegDt(null);
member.setUdtDt(null);
member.setEmailAuthYn(false);
member.setEmailAuthDt(null);
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();
}
============================= 오류 =============================
- 오류: 탈퇴 화면에서 맞는 비밀번호를 입력하나 틀린 비밀번호를 입력하나 둘다 화이트 라벨 페이지로 이동한다.
- 원인: equals()안에 매개변수 위치를 맞춰 써야한다. equals(plainText, hashed) => 순서를 바꿔쓰면 안된다.
<hide/>
if(!PasswordUtils.equals(password, member.getPassword())) { // 탈퇴 불가능 - 자리 바꾸니까 정상 탈퇴
return new ServiceResult(false, "비밀번호가 일치하지 않습니다.");
}
Note) 실행 결과
- 회원 탈퇴 화면
-> 현재 비밀번호가 일치하지 않을 때
-> 비밀번호를 올바르게 입력하면 메인 페이지로 이동한다. 데이터베이스에도 아래와 같이 id 빼고 값이 모두 null로 세팅된다.
'Spring Projcect > 학습 관리 시스템 & 백오피스 구축' 카테고리의 다른 글
최종 구현 화면 - 회원 관리 시스템 (0) | 2022.08.29 |
---|---|
Chapter 17. 강좌 관리 (0) | 2022.08.26 |
Chapter 15. 백 오피스(강좌 신청 처리 구현) (0) | 2022.08.24 |
Chapter 14. 스프링 부트(Spring Boot) (0) | 2022.08.24 |
Chapter 13. 강좌 목록 (0) | 2022.08.23 |