Spring Projcect/학습 관리 시스템 & 백오피스 구축

Chapter 16. 회원 정보 수정

계란💕 2022. 8. 25. 03:03

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로 세팅된다.