Side Project/STUHEL (학습을 돕는 예약시스템)

개발일지 ▲NO.18 ▲ 회원 탈퇴 & 게시글 삭제 - Spring-boot, Oracle, Java, JavaScript, HTML, CSS 부트스트랩, 토이프로젝트

isjiji 2022. 10. 14. 19:21

 

개발을 빼먹고 있었던 두 항목, <회원탈퇴>와 <게시글 삭제>기능. 여기서 기획과 설계의 필요성이 또 느껴진다. 너무 당연한 기능인데도 생각지 못한채 빼먹고 있다가 개발 마지막 단계에서야 기능이 없다는걸 떠올렸고 부랴부랴 개발했다. 

 

두 기능을 추가 개발하는 과정에서 큰 어려움은 없었지만 각 항목별로 중요하게 체크해야하는 부분들이 있었다.

 

1. 게시글 삭제 

  • 게시글 작성자와 게시글 방문자가 동일한 경우에만 삭제 버튼 띄우기
  • 삭제 버튼의 위치 : 게시글에 방해되지 않는 위치이면서도 한눈에 보이는 위치에 삭제 버튼을 두고 싶었다. 본 기능이 게시글 주변에 선명하게 보이면 읽는 행위가 방해될 것이라 생각했고, 숨겨두면 접근성이 떨어져 사용자가 불편 할 것이라 생각했기 때문이다.

 

2. 회원탈퇴

  • 탈퇴 인증 : ID와 PW로 1차 확인을 하고, alert를 띄워서 한번 더 확인하여 쉽게 탈퇴되지 않게 만들어야했다. (실수로 버튼을 클릭할 수도 있기 때문에!)
  • ID, PW 검증시 기존에 있는 모달창을 재사용하고자했다. 이 부분에서 골머리를 썩혔는데, 정말 간단한 문제였다는........
  • 탈퇴버튼 위치 : 탈퇴자 수가 적어야 프로그램 유지이 유지되기 떄문에 탈퇴 버튼을 최대한 눈에 띄지 않게 만들고자했다.

 

 

1. 게시글 삭제하기

오른쪽 위, 네비게이터에 삭제버튼을 만들었다. 게시글 자체와는 멀어져 글 읽기 기능에 방해되지 않은 채 접근성을 높였다.

그리고 session에 저장된 ID와 게시글의 writer ID 를 비교해서 같을 경우에만 삭제버튼을 띄우게 만들었다.

 

 

HTML

display를 none으로 설정해 두고 보이지 않게하는 것을 기본설정으로한다.

<li class="nav-item"><a class="nav-link" id="deleteTextButton" onclick="deleteTextFn()" style="color:tomato;display:none;align:right;">삭제</a></li>

 

JS

1. 게시글이 화면에 뜰 때, Session에 저장된 방문자 ID 와 writer ID를 비교하여 ID가 동일할 경우 삭제버튼 display를 block으로 변경한다.  

→ writer와 visitor 가 동일한 경우에만 삭제버튼 open

 

2. 게시글 삭제버튼이 클릭되면 sweetAlert로 삭제여부를 확인한다.

alert에서 삭제여부를 Okay 하면 서버로 넘어가서 게시글을 삭제시킨다.

이 때 넘어가는 데이터는 글제목, 작성자, 게시글Sequence 이다.

글이 성공적으로 삭제되면 게시글목록화면으로 이동한다.

const deleteTextButton = document.querySelector("#deleteTextButton");

<!--session id 확인-->
if(boardReadData.writer==sessionData.id){
        deleteTextButton.style.display="block";
}


<!--게시글 삭제 function-->
    function deleteTextFn(){
        swal({
            title : '게시글 삭제'
            , text : 'ヽ(✿゚▽゚)ノ \n'
                     +'게시글을 삭제하시겠습니까? \n'
            , buttons : ["취소","확인"]
            , buttonColor : 'tomato'
        }).then(function(result) {
            if(result==null){
                return;
            }else{
                let deleteBoardData = {"title":boardReadData.title,"writer":boardReadData.writer,"noteSeq":boardReadData.noteSeq};
                    deleteBoardData = JSON.stringify(deleteBoardData);
                let xhr = new XMLHttpRequest();
                xhr.open('Delete', "/stuhel/board/deleteBoard?deleteBoardData="+encodeURI(deleteBoardData)
                            ,true);
                xhr.setRequestHeader('Accept', 'application/json');
                xhr.send();
                xhr.onreadystatechange = () => {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        let errorInfo = xhr.responseText;
                            errorInfo = JSON.parse(errorInfo);
                        console.log(errorInfo.errorCd);
                        alert(errorInfo.errorMsg);
                        location.href="/stuhel/board/boardList.html";
                    }
                }
            }
        });
    }

 

Controller

JS에서 보낸 데이터를 gson을 이용해 TO에 담아준다. gson은 적은 데이터를 받고 관리하기에 유용하다.

TO를 ServiceImpl로 넘긴다.

    @DeleteMapping("/deleteBoard")
    HashMap<String,String> deleteBoard(@RequestParam("deleteBoardData") String deleteBoardData, HttpServletRequest request){
        BoardTO board = gson.fromJson(deleteBoardData, BoardTO.class);
        return boardService.deleteBoard(board);
    }

 

Service

1. 게시글을 삭제한다.

2. 게시글에 달린 댓글들을 삭제한다.

조회가 아닌 경우에는 트랜젝션으로 관리하고자 했기 때문에 @Transactional 어노테이션을 사용했다. try 안에 있는 부분은 exception 발생시 모두 취소될 수 있도록 속성을 달아줘야겠다.

    @Override
    @Transactional
    public HashMap<String, String> deleteBoard(BoardTO board) {
        HashMap<String, String> map = new HashMap<>();
        map.put("errorCd","N");
        map.put("errorMsg","게시글이 삭제되었습니다.");
        try {
            boardMapper.deleteBoard(board);
            boardMapper.deleteBoardComment(board); //삭제된 게시글의 댓글 삭제
        }catch (Exception e){
            e.printStackTrace();
            map.put("errorCd","Y");
            map.put("errorMsg","오류발생");
        }
        return map;
    }

 

Mapper

DB에 저장된 해당 글을 삭제한다.

해당 글에 달린 댓글들 또한 삭제한다.

<delete id="deleteBoard" parameterType="com.helper.study.stuhel.board.to.BoardTO">
    DELETE FROM BOARD
    WHERE WRITER = #{writer}
    AND NOTE_SEQ = #{noteSeq}
    AND TITLE = #{title}
</delete>
<delete id="deleteBoardComment" parameterType="com.helper.study.stuhel.board.to.BoardTO">
    DELETE FROM BOARD_COMMENT
    WHERE NOTE_SEQ = #{noteSeq}
</delete>

 

 

 

2. 회원탈퇴기능

 

HTML

화면의 가장 아랫부분인 footer의 가장 아래에 <회원탈퇴>버튼을 올렸다. 회원유출 절대막아...!

<footer class="bg-light py-5">
    <div class="container px-4 px-lg-5"><div class="small text-center text-muted">Copyright &copy; 2022 - isjiji</div></div>
    <div align="right">
        <input type="button" class="btn btn-light btn-xl" id="deleteMemberInfo" onclick="userInfoChange('memberInfoDelete')" value="회원탈퇴" style="margin:20px;background-color:#D5D5D5"/>
    </div>
</footer>

 

 

JS

1. 회원탈퇴 버튼이 클릭

회원탈퇴 버튼이 클릭되면 userInfoChange() 함수가 호출된다.

그리고 인자값으로 문자열 'memberInfoDelete' 를 넘겨준다. 해당 인자값은 회원정보 수정시 사용되는 회원확인모달을 재사용하기 위한 인자값이다.

인자값은 모달의 <확인>버튼의 value로 지정된다.

그래서 현재 사용자가 하고자하는 행위가 <회원탈퇴>인지  <회원정보수정>인지 function이 호출될 때 넘겨지는 인자값으로 구분할 수 있다.

 

2. 모달을 닫을 때

모달의 <확인>버튼의 value로 지정된 값을 비교해서 <회원탈퇴>인지  <회원정보수정>인지 검사한 다음, 행위와 연결된 정보를 지운다.

 

3. 회원정보 확인(ID,PW검사) 및 행위 조건검사 (회원탈퇴 or 회원정보수정)

모달에서 확인버튼을 클릭하면 idPwConfirmFunc()가 호출된다. 

해당 ID와 PW가 현재 로그인한 사용자의 ID,PW와 동일한지 검사한 다음 동일할 경우

회원탈퇴기능을 진행하는 deleteMember() 가 실행된다. 이때도 if문으로 인자값을 검사해 <회원탈퇴>인지  <회원정보수정>인지 검사 한다. 

 

4. 회원탈퇴 함수실행

deleteMember() 함수가 실행되면 먼저 sweetAlert 로 진짜 탈퇴할 것인지 검사를 거친다. 

alert에서 확인버튼이 클릭되면 회원정보를 담아서 서버단에 넘겨준다.

그리고 회원탈퇴가 성공적으로 이뤄지면 locarion.reload(ture);가 실행되고 로그인화면으로 이동한다. 

 

<!--회원 확인 modal창 오픈 ID,PW확인-->
    function userInfoChange(divide){
        userCheckModal.style.display="flex";
        checkId.focus();
        idPwConfirm.value=divide;
    }

<!--모달 닫기-->
    function modalClose(modalType){
        if(modalType.value=="userCheckModalClose"){
            userCheckModal.style.display = "none";
            checkPw.value="";
            checkId.value="";
            checkMsg.innerHTML="";
            return;
            }
        if(modalType.value=="bookCondiModalClose" || modalType=="bookCondiModalClose"){
            bookConditionModal.style.display = "none";
            bookCModalYear.innerHTML="";
            bookCModalMonth.innerHTML="";
            bookRoomAndTimeDiv.innerHTML="";
            return;
        }
    }
    
<!--회원정보 모달 ID,PW 회원인증-->
    function idPwConfirmFunc(divide){
    console.log("확인버튼 클릭");
    console.log(divide);
        if(checkPw.value.trim()==null||checkPw.value.trim()=="" ||
           checkId.value.trim()==null||checkId.value.trim()==""
        ){
           checkMsg.innerHTML = "※ ID 혹은 비밀번호를 입력해주세요";
           return;
        }else if(dbUserId!=checkId.value.trim()){
           checkMsg.innerHTML = "※ 잘못된 ID를 입력하셨습니다.";
           return;
        }else{
           let loginData = {"password":checkPw.value, "id":checkId.value};
           loginData = JSON.stringify(loginData);

           let xhr = new XMLHttpRequest();
           xhr.open('GET', "/member/login?"
               + "loginData=" + encodeURI(loginData)
               ,true);
           xhr.setRequestHeader('Accept', 'application/json');
           xhr.send();
           xhr.onreadystatechange = () => {
               if (xhr.readyState == 4 && xhr.status == 200) {
                   // 데이터 확인
                   let txt = xhr.responseText;
                   txt = JSON.parse(txt);
                   if (txt.errorCd == 'Y') {
                        checkMsg.innerHTML = "※ " + txt.errorMsg;
                        return;
                   } else {
                        userCheckModal.style.display = "none";
                        checkPw.value="";
                        checkId.value="";
                        checkMsg.innerHTML="";
                        console.log(divide);
                        
                <!--[회원탈퇴]인지 [회원정보수정]인지 검사-->
                        if(divide=="memberInfoChange"){
                            memberInfoChangeFunc();
                        }else if(divide=="memberInfoDelete"){
                            deleteMember();
                        }
                   }
               }
           }
       }
   }

    
<!--회원탈퇴-->
    function deleteMember(){
            swal({
                  title : '회원 탈퇴'
                , text : 'ヽ(✿゚▽゚)ノ \n'
                        +'정말 탈퇴하시겠습니까?'
                , buttons : ["취소","확인"]
            }).then(function(result){
                console.log(result);
                if(result==null) return;
                else {
                    let deleteMemberInfo = {"id":dbUserId,"name":dbUserName,"birthDay":dbUserBirth} //데이터 추가하기
                    console.log(deleteMemberInfo);
                    deleteMemberInfo = JSON.stringify(deleteMemberInfo);

                    let xhr = new XMLHttpRequest();
                    xhr.open('DELETE',  "/stuhel/myPage/deleteMember?"
                                        + "deleteMemberInfo=" + encodeURI(deleteMemberInfo)
                            ,true);
                    xhr.setRequestHeader('Accept', 'application/json');
                    xhr.send();
                    xhr.onreadystatechange = () => {
                        if (xhr.readyState == 4 && xhr.status == 200) {
                            // 데이터 확인
                            let errorInfo = xhr.responseText;
                            errorInfo = JSON.parse(errorInfo);
                            console.log(errorInfo.errorCd);
                            console.log(errorInfo.errorMsg);
                            if (errorInfo.errorCd== "Y" ) {
                                alert(errorInfo.errorMsg);
                                return;
                            } else {
                                alert("탈퇴되었습니다.");
                                logoutButton();
                                location.reload(true);
                            }
                        }
                    }
                }
            });
    }

 

Controller

JS에서 보낸 데이터를 gson을 이용해 TO에 담아준다. gson은 적은 데이터를 받고 관리하기에 유용하다.

TO를 ServiceImpl로 넘긴다.

@DeleteMapping("/deleteMember")
HashMap<String,String> deleteMember(HttpServletRequest request, @RequestParam("deleteMemberInfo") String deleteMemberInfo){
    MemberTO memberTO = gson.fromJson(deleteMemberInfo, MemberTO.class);
    return myPageService.deleteMemberInfo(memberTO);
}

 

Service

mapper가 총 네번 호출된다. 

1. 탈퇴 요청한 회원정보 삭제

2. 탈퇴 요청한 회원의 댓글 삭제

3. 탈퇴 요청한 회원의 게시글 삭제

4. 탈퇴 요청한 회원의 세미나실 예약정보 삭제

여기서 또한 Transactional 어노테이션이 호출되는데, 한번의 transaction 안에 실행될 수 있도록 속성을 걸어줘야겠다.

@Override
@Transactional
public HashMap<String, String> deleteMemberInfo(MemberTO memberTO) {
    HashMap<String, String> map = new HashMap<>();
    map.put("errorCd","N");
    try{
        myPageMapper.deleteMemberInfo(memberTO);  //탈퇴요청회원 정보삭제
        myPageMapper.deleteMemberBoardCommentData(memberTO);  //탈퇴요청회원 댓글삭제
        myPageMapper.deleteMemberBoardData(memberTO);   //탈퇴요청회원 게시글삭제
        myPageMapper.deleteMemberRoomBookData(memberTO);    //탈퇴요청회원 예약현황삭제
    }catch(Exception e){
        e.printStackTrace();
        map.put("errorCd","Y");
        map.put("errorMsg","탈퇴중 오류가 발생했습니다. 잠시후 다시 시도해주세요.");
    }
    return map;
}

 

Mapper

 

<delete id="deleteMemberInfo" parameterType="com.helper.study.stuhel.member.to.MemberTO">
    DELETE MEMBER_SECURITY
    WHERE ID=#{id} AND NAME=#{name} AND BIRTH=#{birth}
</delete>
<delete id="deleteMemberBoardCommentData" parameterType="com.helper.study.stuhel.member.to.MemberTO">
    DELETE FROM BOARD_COMMENT
    WHERE WRITER=#{id}
    OR CGROUP IN (SELECT COMMENT_SEQ  FROM BOARD_COMMENT WHERE WRITER=#{id})
    OR NOTE_SEQ IN (SELECT NOTE_SEQ  FROM BOARD WHERE WRITER=#{id})
</delete>
<delete id="deleteMemberBoardData" parameterType="com.helper.study.stuhel.member.to.MemberTO">
    DELETE FROM BOARD WHERE WRITER=#{id}
</delete>
<delete id="deleteMemberRoomBookData" parameterType="com.helper.study.stuhel.member.to.MemberTO">
    DELETE FROM ROOM_BOOK WHERE USER_ID=#{id}
</delete>