카테고리 없음

게시판 기능 구현 5 : 글 보기 - 스프링, jsp, 오라클, mybatis

blogOwner 2024. 3. 25. 01:49

이번 포스팅에서는 2가지 일을 할것이다. 

첫번째는 아래와 같은 게시판 홈에서 제목을 클릭했을 때, 게시글의 상세가 보여지는 과정을 구현해볼 것이다.  

두번째는 위와 같이 제목을 클릭해서 들어간 게시글 상세페이지에서 추천을 누르는 걸 구현해 볼 것이다.

----------------------------------------------------------------------------------------------------------------------

 

일단, boardHome.jsp 파일(게시판홈) 중 아래 부분이 게시글들을 렌더링해주는 부분이었다. 

<c:forEach var="post" items="${currentPagePosts}">
    <div class="post-row"> <!-- 각 게시물을 감싸는 div 추가 -->
        <span>${post.id}</span>
        <a href="#" class="post-link" data-postid="${post.id}">
            <c:if test="${post.isPrivate == 1}">
                <i class="fa-solid fa-lock" style="color:red;"></i>
            </c:if>
            ${post.title}
        </a>
        <span>${post.userId}</span>
        <span>${post.writingDate}</span>
        <span>${post.viewNumber}</span>
    </div>
</c:forEach>

여기서 제목을 누르는 순간, fetch 문이 실행되도록 하였다. 아래와 같다. 

post-link 라는 class 를 지닌 모든 요소를 querySelectorAll 로 선택한 다음에, .forEach 문을 이용해서 반복을 돌리고 있다. 

만약, 그 a 태그(post-link 라는 class 를 지닌 요소. 즉 제목 을 말함)를 클릭하면, 기본 anchor 태그 동작인 get 요청 보내는 걸 막고, 그 a태그에 저장해둔 data-postid 라는 키에 담긴 값을 가져온다.

무슨 소리냐면,     위 코드 중    <a href="#" class="post-link" data-postid="${post.id}"> 이 부분에 빨강색 글씨 부분처럼 하면 데이터를 a 태그에 담아둘 수 있다. 

data-* 문법은 HTML5 에서 나온 문법인데, 이렇게 태그에 값을 저장하고 꺼내서 쓸 수 있도록 해준다. 지금 data-postid 라는 키에 담긴 값은 그 게시글의 posts테이블의 기본키인 id 컬럼값이다. 

그리고 나서는 fetch 문을 통해 ajax 통신을 한다. 

이 fetch 문의 목적은 무엇이냐면, 만약, 비밀글인 경우, 비밀번호를 알고 있는지 확인받고 게시글을 보여주기 위함이다. 

<script>
    document.addEventListener('DOMContentLoaded', function(){
        document.querySelectorAll('.post-link').forEach(function(element){
            element.addEventListener('click', function(event){
                event.preventDefault(); // 기본 앵커 태그 동작 방지.

                var postId = this.getAttribute('data-postid');

                //우선, 글을 클릭한 사용자가 글을 쓴 사람과 같은 사람인지부터 확인해줘야함.
                //동일인이라면, 굳이 비밀번호를 안물어보기 위함이다.
                fetch('/is-this-you?postId=' + postId)
                    .then(response => {
                        if(!response.ok){
                            throw new Error('Network response was not ok');
                        }
                        return response.text();
                    })
                    .then(data => {
                        console.log(data);

                        if(data == 'yes'){
                            window.location.href="/show-writing?postId="+postId;
                        }else{
                             //fetch 이용해서 통신. 비밀글이 있는지 여부를 확인해줄거임.
                            fetch('/is-this-secret?postId=' + postId)
                                .then(response => {
                                    if(!response.ok){
                                        throw new Error('Network response was not ok');
                                    }
                                    return response.json();
                                })
                                .then(data => {
                                    console.log(data.isThisSecret);
                                    if(data.isThisSecret == 'no'){ // 비밀글이 아닌 경우
                                        window.location.href="/show-writing?postId="+postId;
                                    }else{ //비밀글인 경우

                                        let input = prompt('게시물의 비밀번호를 입력하세요');
                                        if(input == data.writingPassword){ //비밀번호가 일치한다면
                                            window.location.href="/show-writing?postId="+postId;
                                        } else{
                                            alert('비밀번호가 틀렸습니다.');
                                        }
                                    }
                                })
                                .catch(error => console.error('There has been a problem with your fetch operation:', error));
                        }
                    })
                    .catch(error => console.error('There has been a problem with your fetch operation:', error));



            });
        })
    });
    </script>

fetch 문을 보면, localhost:8080/is-this-you 라는 url 로 get요청을 보내면서, 방금 얻은 posts 테이블의 id 컬럼값을 보내줬다. 

이 get 요청을 받은 컨트롤러는 아래와 같이 생겼다. 

@GetMapping("/is-this-you")
@ResponseBody
public String isThisYou(@RequestParam String postId, HttpServletRequest request){
    //게시글을 클릭한 사람이 본인인지 확인.
    PostsDTO post = postsMapper.findPostById(Integer.parseInt(postId));
    int memberId = post.getMemberId();

    int loginedMember = FindLoginedMemberIdUtil.findLoginedMember(request);

    if (memberId == loginedMember) {
        return "yes";
    } else{
        return "no";
    }
}

간단하게 말하자면,  넘겨받은 posts 테이블의 id 컬럼으로 posts 테이블에서 그 행을 조회한다. 

그리고 그 행에서 member_id 컬럼의 값을 꺼내와서, 현재 HTTP 요청(get요청)을 보낸 사람과 일치하는지 확인한다. 

일치한다면 yes 를 리턴해주고, 일치하지 않는다면 no 를 리턴해준다. 

참고로, findPostById 라는 메서드로 인해 실행되는 쿼리는 아래와 같고,

더보기

//PostsMapper (@Mapper 붙은 인터페이스) 내 메서드

 

public PostsDTO findPostById(Integer postId);

 

// PostsMapper.xml

<select id="findPostById" resultType="PostsDTO">
SELECT * FROM posts
WHERE id = #{postId}
AND DELETE_POST_FL = 'N'
</select>

FindLoginedMemberIdUtil 클래스는 아래와 같이 생겼다. 

더보기

public class FindLoginedMemberIdUtil {
public static Integer findLoginedMember(HttpServletRequest request){
HttpSession session = request.getSession(false);
MemberJoinDTO loginedMember = (MemberJoinDTO) session.getAttribute("loginedMember");
Integer id = loginedMember.getId();
return id;
}
}

다시 fetch 문으로 돌아가보자

돌아오고 나서의 코드는 아래와 같다. 

if(data == 'yes'){
    window.location.href="/show-writing?postId="+postId;
}else{
     //fetch 이용해서 통신. 비밀글이 있는지 여부를 확인해줄거임.
    fetch('/is-this-secret?postId=' + postId)
        .then(response => {
            if(!response.ok){
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .then(data => {
            console.log(data.isThisSecret);
            if(data.isThisSecret == 'no'){ // 비밀글이 아닌 경우
                window.location.href="/show-writing?postId="+postId;
            }else{ //비밀글인 경우

                let input = prompt('게시물의 비밀번호를 입력하세요');
                if(input == data.writingPassword){ //비밀번호가 일치한다면
                    window.location.href="/show-writing?postId="+postId;
                } else{
                    alert('비밀번호가 틀렸습니다.');
                }
            }
        })
        .catch(error => console.error('There has been a problem with your fetch operation:', error));
}

 

만약, yes 를 리턴받은 경우에는, localhost:8080/show-writing 이라는 url 로 get 요청을 보내면서 posts 테이블의 id 컬럼값을 쿼리스트링으로 보내주고 있다. 해당 글의 작성자와 fetch 로 get요청 보낸 사람이 동일하다면 자신이 쓴 글이니까 비밀글이든 일반글이든 그냥 보여주도록 함. 

 

근데, no 를 리턴받은 경우에는, 비밀글인지 여부를 확인해야된다. 그래서, 다시 한번 localhost:8080/is-this-secret 으로 get 요청을 보낸다. 이때 get요청의 쿼리스트링으로는 클릭한 그 게시글의 posts 테이블 id 컬럼값을 보내준다. 

이 get 요청을 받는 컨트롤러는 아래와 같다.

@GetMapping("/is-this-secret")
@ResponseBody
public Map<String,String> isThisSecret(@RequestParam String postId){
    //비밀글인지 여부를 조사.
    PostPasswordDTO row = postPasswordMapper.findRow(Integer.parseInt(postId));
    Map<String, String> map = new ConcurrentHashMap<>();

    // 만약 처음에 비밀글로 생성하지 않았다면, post_password 테이블에 행이 아예 존재하지 않을 수 있거든.
    // 그래서, row 가 null 이 아니라면 => 비밀글로 생성했다는 뜻.
    // row 가 null 이면 => 비밀글로 생성하지 않았다.
    if (row == null) {
        // 공개글이다.
        map.put("isThisSecret", "no");
    } else{
        // 비밀글이다.
        String postPassword = row.getPostPassword();
        map.put("isThisSecret", "yes");
        map.put("writingPassword", postPassword);
    }

    return map; //비밀글 이다.
}

간단히 정리해두자면, POST_PASSWORD 테이블에서 행을 조회하는데, post_id 컬럼값이 매개변수로 넘겨받은 posts테이블의 id 컬럼과 동일한 값을 가지고 있는 행을 조회한다. 사용자가 게시글을 작성해서 insert 할 당시, 사용자가 공개글로 하겠다고 해서 게시글을 비밀번호를 안쓴 경우, POST_PASSWORD 테이블에 행이 안들어갔을 가능성이 있거든?

만약, 하나도 조회되지 않아서, row 변수가 null 인 경우(즉, 공개글로 작성된 경우), map 에 isThisSecret 이라는 키에 no 라는 값을 넣어주고, row 변수가 null 이 아닌 경우에는 비밀글이라는 뜻이기 때문에, isThisSecret 이라는 키에는 yes 를 담아주고, writingPassword 라는 키의 값으로는 그 게시글의 비밀번호를 담아주었다.

 

다시 fetch 문으로 돌아오면, 아래와 같다. 

if(data.isThisSecret == 'no'){ // 비밀글이 아닌 경우
    window.location.href="/show-writing?postId="+postId;
}else{ //비밀글인 경우

    let input = prompt('게시물의 비밀번호를 입력하세요');
    if(input == data.writingPassword){ //비밀번호가 일치한다면
        window.location.href="/show-writing?postId="+postId;
    } else{
        alert('비밀번호가 틀렸습니다.');
    }
}

만약 isThisSecret 에 "no" 가 들어있었을 경우, 즉 공개글인 경우에는 곧바로 해당 게시글을 보여달라 get요청을 했고, 

isThisSecret 에 "yes" 가 들어있었을 경우, 즉 비공개글인 경우에는 prompt 창으로 비밀번호를 입력받아 만약 컨트롤러에서 

map.put("writingPassword", postPassword); 이렇게 담아줬던 writingPassword 와 일치하는 경우에만 해당 게시글을 보여달라는  get 요청을 했다. 그리고, 비밀번호가 틀리다면 alert창으로 '비밀번호가 틀렸습니다' 라고 안내했다. 

 

본인이 쓴 글이 아니면서, 비밀글인데 비밀번호를 틀린 경우를 제외하고서는 localhost:8080/show-writing 로 get 요청을 하면서 쿼리스트링으로 클릭한 게시글의 posts 테이블 id 컬럼값을 보내고 있다. 

그렇다면, 이를 받은 /show-writing 이라는 url 을 매핑하는 컨트롤러는 어떤 일을 해주어야 할까? 

 

그 컨트롤러가 해야 할 것들을 미리 정리해 둔다. 

최종적으로 그 컨트롤러가 해야 하는 건, 클릭할 그 게시물을 사용자에게 보여주는 것이다. 

아래 사진과 같이 말이다. 

 

이 페이지를 보여주려면 뭘 model 에 담아줘야 할까? 

1. 이 글을 클릭한 사용자의 회원 정보가 필요. 왜? 맨 밑에 보면, 좀 잘려있는데, 댓글 쓰는 창에 보면, 로그인 사용자의 아이디를 보여주거든. 아래 그림은 네이버의 댓글창인데, 여기서도 로그인한 나의 아이디를 보여주고 있다. 

2. posts 테이블의 모든 컬럼이 필요. 

3. 이 posts 테이블의 행과 1:1 관계인 post_like(좋아요, 추천) 테이블의 like_number 컬럼 값이 필요하다. 그게 있어야, 지금 추천수를 렌더링해줄 수 있기 때문이다. 그런데, 게시글 작성자가 게시글을 작성할 때에, db 낭비를 안하려고 추천을 눌렀을 때에야 비로소 그 posts테이블의 행과 연관된 post_like 테이블 행을 insert 해주기로 했었거든.

그래서 이 컨트롤러에서 어떤 작업을 해줘야 하냐면, 매개변수로 받은 posts 테이블의 id 컬럼값을 가지고 post_like 에 행을 조회한다. 만약 행이 없어서 null 값이라면, 0이 렌더링 될 수 있도록 0을 넣어주는 작업을 해야한다.

 

이 외에도 이 posts 테이블에 얽혀있는 comments 테이블의 행들도 가져와야 하는데, 그건 이후의 포스팅에서 하도록 하고, 일단 이렇게 3가지의 일을 해야 한다.

 

그리고 한 가지 더 해야한다. 뭐냐면, 조회수 올리는 작업. 

일단, localhost:8080/show-writing 이라는 url 을 매핑하는 컨트롤러는 아래와 같이 생겼다.

다른 건 신경쓸 필요가 아직 없고, showPostService 라는 서비스계층의 showPost 메서드를 호출하고 나서, showPost.jsp 파일로 이동하는 걸 확인할 수 있다. model 에 뭐 담고, 조회수 올리는 작업은 showPostService 클래스의 showPost 메서드가 행한다. 

@GetMapping("show-writing")
public String showWritingControllerMethod(@RequestParam(required = false, defaultValue = "1") Integer page,
                                          @RequestParam Integer postId,
                                          Model model,
                                          HttpServletRequest request) {

    showPostService.showPost(page, postId, model, request);
    return "/contact/showPost";
}

 

ShowPostService 클래스의 showPost 메서드

public void showPost(Integer page, Integer postId, Model model, HttpServletRequest request){

    // 1. 이 글을 클릭한 사용자의 회원정보를 세션객체에서 얻어와서 model 에 담아주도록 한다.
        HttpSession session = request.getSession(false);
        MemberJoinDTO loginedMember = (MemberJoinDTO) session.getAttribute("loginedMember");
        model.addAttribute("loginedMemberId", loginedMember);

    // 2-1. 매개변수로 받은 postId(posts 테이블의 id 컬럼값) 으로 post테이블에서 행을 조회한다.
        PostsDTO findPost = postsMapper.findPostById(postId);

    // 2-2. 현재 findPost 에는 member_id 컬럼값이라고 해서 게시글을 작성한 사용자의 member테이블 id 컬럼값이 들어있다.
    // 그런데, 이 id 컬럼값은 시퀀스 값으로서 아무런 의미가 없는 값((ex: 237 이런 값이다))이기 때문에,
    // member테이블에서 이 id 컬럼값을 가진 행의 user_id 컬럼값(ex : wowns590 이런 값이다)을 가져와서, findPost의 userId 필드에 바인딩한다.
        int memberId = findPost.getMemberId();
        String userId = memberMapper.findUserIdById(memberId);
        findPost.setUserId(userId);
    // 2-3. model 에 담아준다.
        model.addAttribute("findPost", findPost);

    // 3. 조회수와 관련된 작업.
    // 처음 게시글이 작성되어 posts 테이블에 행을 insert 할 때, post_view 테이블에 행을 넣지 않았다.
    // 따라서, 현재 이 게시글을 클릭한 것이 처음인 경우, 이 게시글에 대한 post_view 테이블의 행은 존재하지 않는다.
    // 3-1. 매개변수로 넘어온 posts 테이블의 id 컬럼값을 이용해서 post_view 테이블에서 행을 조회한다.
        Integer viewNumber = postViewMapper.findPostViewCountByPostId(postId); // postId 로 post_view 테이블의 view_number 컬럼값을 조회.

    // 3-2.
    // 만약, null 이라면, 최초로 이 게시글이 조회된 것이므로, post_view 테이블에 행을 삽입하고, 조회수를 1 올린다.
    // 만약, null 이 아니라면, 이미 바로 위의 과정을 거친것이기 때문에, 조회수만 1 올려주면 된다.
        if (viewNumber == null) {
            // 최초 조회.
            Integer sequence = postViewMapper.selectNextSequenceValue();
            postViewMapper.initPostView(sequence, postId);
        }

    // 3-3. 새로 post_view 테이블에 행을 삽입했든, 원래 있었든, 조회수 1을 올려줄 것이다. 근데, 조건이 있다.
    // "게시글을 클릭한 사용자 != 게시글을 작성한 사용자" 이어야 한다.
        if (memberId != loginedMember.getId()) {
            postViewMapper.updateByPostId(postId);
        }

    // 4. 좋아요수와 관련된 작업
    // 좋아요 수도 조회수와 마찬가지로 처음 게시글이 작성되어 posts 테이블에 행을 insert 할 때, post_like 테이블에 행을 삽입하도록 하지 않았다.
    // 따라서, 아직 누구도 추천(좋아요)을 누르지 않은 경우라면, post_like 테이블에 행이 존재하지 않음.
    // 4-1. 매개변수로 넘어온 posts 테이블의 id 컬럼값을 이용해서 post_like 테이블에서 행을 조회한다.
        Integer findPostLikeNumber = postLikeMapper.findPostLikeCountByPostId(postId);

    // 4-2.
    // 만약, null 이라면 model 에 likeNumber 라는 키에 0을 넣어준다.
    // 만약, null 이 아니라면, model 에 likeNumber 라는 키에 조회된 post_like 테이블의 행에서 like_number 값을 넣어준다.
    if (findPostLikeNumber == null) {
        model.addAttribute("likeNumber", 0);
    } else{
        model.addAttribute("likeNumber", findPostLikeNumber);
    }
}

showPost.jsp 파일은 아래와 같이 생겼다. 

더보기
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<c:import url="/jsp/bootstrapconfig/index.jsp"/>

<html>
<head>
    <link rel="stylesheet" href="../../css/board/showPost.css">
</head>
<body>


<!--네브 바 -->
    <c:import url="/jsp/common/loginedNavbar2.jsp" />
<!--네브 바 종료 -->

    <div id="writing-area">
        <div id="page-title">
            ${findPost.title}
        </div>

        <div id="first">
            <!-- 비밀글로 할건지 여부 + 비밀번호 input, 등록 버튼 -->
            <div></div>
            <div>

                <div>
                </div>
            </div>
            <div>
                <div style="width:100%; ${show ? 'display:block;' : 'display:none;'} " id="hiddenPasswordInput">

                </div>
            </div>
            <div>
                <a href="/board-home" class="button-in-middle-container" id="board-home-btn" style="margin-top:0.3vh;"> 글 목록으로 </a>
            </div>
            <div></div>
        </div>

        <div id="second">
            <!-- 작성자(유저 아이디) + 제목 -->
            <div></div>
            <div>
                <div>
                작성자 : ${findPost.userId}
                </div>
                <div>
                </div>
            </div>
            <div></div>
        </div>

        <div id="third">
            <div></div>
            <div>
                <textarea path="content" style="width:100%; height:60%; border:1px solid lightgray;  resize: none;" readOnly>${findPost.content}</textarea>
                <div style="width: 100%; height: 10%;"></div>
                <div id="thumb-up-div"><span><i class="fa-solid fa-thumbs-up"></i> &nbsp 추천</span> &nbsp &nbsp <span id="like-number">${likeNumber}</span> </div>
            </div>
            <div></div>
        </div>
    </div>

    <div id="post-update-and-delete">
        <div></div>
        <div>
        <c:if test="${loginedMemberId.id==findPost.memberId}">
            <a href="/update-post?postId=${findPost.id}" >글 수정</a>
            <a href="/delete-post?postId=${findPost.id}" id="delete-post-btn">글 삭제</a> <!-- delete 는 ajax 로 처리함-->
        </c:if>
        </div>
        <div></div>
    </div>

    <!-- ---------------------------comment------------------------------ -->
    <div id="comment-container">

        <div id="comment-insert-form-div">
            <div></div>
            <div>
                <div id="comment-id-div">
                    <div>

                    </div>
                    <div>
                        ${loginedMemberId.userId}
                    </div>
                </div>
                <div id="comment-content-writing-div">
                    <form action="/insert-comment" method="post">
                    <textarea name="content" id="comment-textarea" style="resize:none;"></textarea>
                    <input type="hidden" name="postId" value="${findPost.id}"/>
                    <input type="hidden" name="memberId" value="${loginedMemberId.userId}"/>
                </div>
                <div id="comment-register-btn-div">
                    <div>
                        <button type="submit" class="button-register" id="comment-btn" style="margin-top:0.3vh;"> 등록 </button>
                    </div>
                    </form>
                </div>
            </div>
            <div></div>
        </div>

        <!-- 댓글 대댓글 표시하기 -->
        <div id="comment-reply-area">
            <div></div>
            <div>
                <c:forEach var="comment" items="${currentPagePosts}" >
                    <div style="width:100%; margin-top: 1vh; border-top: 1px solid lightgray;">
                            <div>
                                <span style="font-weight: bold; font-size: 17px;">
                                    ${comment.userId}
                                </span>
                                <span style="font-size: 9px;">
                                   ${comment.stringCreateDate}
                                </span>
                            </div>
                            <div>

<!-------------------------------------------------------------------------------------------->
                                <!-- 조건문 추가: 로그인한 사용자가 댓글 작성자와 같다면 "삭제" 표시 -->
                                <c:if test="${loginedMemberId.userId == comment.userId}">
                                <div>
                                    <div style="font-size: 12px;" class="comment-content-div">
                                        ${comment.content}
                                    </div>
                                    <div style="display:none;">
                                        <form action="/update-comment" method="post" class="update-form">
                                            <textarea name="content" class="update-comment-textarea">${comment.content}</textarea>
                                            <input type="hidden" name="postId" value="${findPost.id}">
                                            <input type="hidden" name="commentId" value="${comment.id}" >
                                            <button class="update-btn">수정하기</button>
                                        </form>
                                    </div>

                                    <span type="button" style="color:red; font-size: 12px; cursor:pointer;" class="update-comment">수정</span>
                                    <span style="color:red; font-size: 12px; cursor:pointer;" class="delete-comment" data-comment-id="${comment.id}">삭제</span>
                                </div>
                                </c:if>
<!-------------------------------------------------------------------------------------------->
                                <button class="click-me" >댓글 쓰기</button>
                                <div class="textarea-reply" style="display:none;">
                                    <form action="/reply-save" method="post">
                                        <textarea name="content"   style="width:60%;  height: 11vh; resize:none;"></textarea>
                                        <input type="hidden" name="postId" value="${findPost.id}">
                                        <input type="hidden" name="memberId" value="${loginedMemberId.id}">
                                        <input type="hidden" name="parentCommentId" value="${comment.id}">
                                        <button type="submit" class="button-register">등록</button>
                                    </form>
                                </div>

                                <div style="width:100%; height: 1vh;"></div>

                                <c:forEach var="reply" items="${comment.replies}">

                                    <div class="reply" style="margin-left: 2vw; background-color: #fafafa; >
                                         &nbsp  <span style="font-weight: bold; font-size: 17px;">  ${reply.userId}</span> <span style="font-size: 9px; ">${reply.stringCreateDate}</span>
                                    </div>




<!-------------------------------------------------------------------------------------------->
                                <c:if test="${loginedMemberId.userId == reply.userId}">
                                    <div>
                                        <div class="reply" style="margin-left: 2vw; font-size: 12px; background-color: #fafafa; border-bottom:0.5px solid lightgray;">
                                             ${reply.content}
                                        </div>
                                        <div style="display:none;">
                                              <form action="/update-comment" method="post" class="update-form">
                                                  <textarea name="content" class="update-comment-textarea">${reply.content}</textarea>

                                                  <input type="hidden" name="commentId" value="${reply.id}" >
                                                  <button class="update-btn">수정하기</button>
                                              </form>
                                        </div>
                                        <span style="color:red; font-size: 12px; margin-left: 2vw; cursor:pointer;" class="update-reply">수정</span>
                                        <span style="color:red; font-size: 12px; cursor:pointer;" class="delete-comment" data-comment-id="${reply.id}">삭제</span>

                                    </div>
                                </c:if>
<!-------------------------------------------------------------------------------------------->

                                </c:forEach>

                            </div>


                    </div>
                </c:forEach>
            </div>


            <div>
            </div>
        </div>


        <!-- pagination -->
        <div >
        </div>


    </div>
    <div id="pagination">
        <c:if test="${currentGroupFirstPage != 1}">
            <a href="/show-writing?page=${currentGroupFirstPage-pageGroupSize}&postId=${findPost.id}"> &laquo; 이전</a>
        </c:if>
        &nbsp &nbsp

        <c:forEach var="i" begin="${currentGroupFirstPage}" end="${currentGroupLastPage}">
            <c:choose>
                <c:when test="${i == currentPage}">
                    <span>${i}</span>
                </c:when>
                <c:otherwise>
                    <a href="/show-writing?page=${i}&postId=${findPost.id}" style="color:black;">${i}</a>
                </c:otherwise>
            </c:choose>
            &nbsp
        </c:forEach>

        &nbsp &nbsp

        <c:if test="${currentGroupLastPage != totalPageCount}">
            <a href="/show-writing?page=${currentGroupLastPage + 1}&postId=${findPost.id}">다음 &raquo;</a>

        </c:if>
    </div>

<script>
// 모든 'click-me' 버튼에 대해 이벤트 리스너를 설정합니다.
document.querySelectorAll('.click-me').forEach(function(button) {
    button.addEventListener('click', function() {
        // 현재 클릭된 버튼과 관련된 'textarea-reply' 요소만 선택합니다.
        // 이 예제에서는 버튼이 'textarea-reply' 요소 바로 전에 있다고 가정합니다.
        let textareaReply = this.nextElementSibling;

        // 'textarea-reply' 요소의 display 상태를 토글합니다.
        textareaReply.style.display = textareaReply.style.display === 'block' ? 'none' : 'block';
    });
});

//추천 누르면, 숫자 올라가게 하면서, ajax 통신해서 db에 +1 증가시키기.
document.addEventListener("DOMContentLoaded", function(){
    $("#thumb-up-div").click(function() {
        fetch('/update-and-find-like-num', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            postId: ${findPost.id},
          }),
        })
        .then(response => {
            if(!response.ok){
                throw new Error('Network response not ok');
            }
            console.log('여기 문제있음2');
            return response.json();
        })
        .then(data => {
            if(data.updatedLikeNumber == -1){
                alert('좋아요 는 1번만 누르실 수 있습니다');
            }else{
                // 여기서 가져온 updatedLikeNumber 를 출력한다.
                document.querySelector('#like-number').innerText = data.updatedLikeNumber;
            }
        })
        .catch(error => console.error('There has been a problem with your fetch operation:', error));
    })
});

// [comment + reply] update form ajax
document.addEventListener("DOMContentLoaded", function(){
    //지금 form 이 하나가 아니므로, 반복을 돌리면서, form 이 submit 됬을 때를 생각.
    document.querySelectorAll('.update-form').forEach(function(element){
        element.addEventListener('submit', function(e){
            e.preventDefault();
            const formData = new FormData(element);

            //json 통신 시도.
            const data = {};
            formData.forEach( (value, key) => { //(key, value) 가 아님을 주의
                data[key] = value;
            });

            fetch('/update-comment', {
                method: 'POST',
                headers: {
                    'Content-Type' : 'application/json', //서버에 json 형식임을 알리는 것.
                },
                body: JSON.stringify(data), //직렬화된 JSON 문자열
            })
            .then(response =>{
                return response.json();
            })
            .then(data => {
                element.parentElement.style.display = 'none';
                element.parentElement.previousElementSibling.style.display = 'block';
                element.parentElement.previousElementSibling.innerText = data.content;
            })
            .catch(error => {
                console.error('There has been a problem with your fetch operation:', error);
            });
        });
    });
});



// [comment + reply] delete span ajax
document.addEventListener("DOMContentLoaded", function() {
    document.querySelectorAll('.delete-comment').forEach(function(element) {
        element.addEventListener('click', function() {
            let commentId= element.dataset.commentId;
            const data = {id : commentId};
            fetch('/delete-comment', {
                method: 'POST',
                headers: {
                    'Content-Type' : 'application/json',
                },
                body: JSON.stringify(data),
            })
            .then(response => {
                return response.json();
            })
            .then(data => {
                alert('댓글이 삭제되었습니다.');
                element.innerText = ' ';
                element.previousElementSibling.innerText = ' ';
                element.previousElementSibling.previousElementSibling.previousElementSibling.innerText = data.text;
            })
        });
    });
});

// [comment] 삭제된 댓글입니다 표시
document.querySelectorAll('.comment-content-div').forEach(comment => {
    const content = comment.innerText;
    if (content.trim() === "삭제된 댓글입니다.") {
        comment.nextElementSibling.nextElementSibling.innerText = ' ';
        comment.nextElementSibling.nextElementSibling.nextElementSibling.innerText = ' ';
    }
});

// [reply] 삭제된 댓글입니다 표시
document.querySelectorAll('.reply').forEach(comment => {
    const content = comment.innerText;
    if (content.trim() === "삭제된 댓글입니다.") {
        comment.nextElementSibling.nextElementSibling.innerText = ' ';
        comment.nextElementSibling.nextElementSibling.nextElementSibling.innerText = ' ';
    }
});

// 게시글 delete 버튼 눌렀을 때
document.addEventListener("DOMContentLoaded", function() {

    let deleteBtn = document.getElementById('delete-post-btn');
    if(deleteBtn != null){
        document.getElementById('delete-post-btn').addEventListener('click', function(e) {
            e.preventDefault(); // <a> 태그의 기본 동작을 방지합니다.

            // fetch를 사용하여 서버에 비동기 요청을 보냅니다.
            fetch('/delete-post?postId=${findPost.id}')
            .then(response => {
                if(!response.ok) {
                    throw new Error('Network response was not ok');
                }
                return response.text();
            })
            .then(data => {
                if(data == 'success'){
                    // 삭제요청을 한 사용자와 게시글의 작성자가 동일인물인 경우
                    if(confirm('정말 삭제하시겠습니까?')){
                        //사용자가 확인을 클릭한 경우
                        fetch('/delete-post-real?postId=${findPost.id}')
                        .then(response => {
                            if(!response.ok) {
                                throw new Error('Network response was not ok');
                            }
                            return response.text();
                        })
                        .then(data => {
                            if(data == 'success'){
                                alert('게시글이 삭제되었습니다');
                                window.location.href="/board-home";
                            } else{
                                alert('게시글 삭제 수행 중 오류가 발생하여 정상 수행되지 못했습니다.');
                            }

                        })
                        .catch(error => console.error('There has been a problem with your fetch operation:', error));
                    }else{
                        //사용자가 취소를 클릭한 경우
                        alert('삭제를 취소하셨습니다');
                        return;
                    }
                } else{
                    alert('부적절한 접근 : 작성자만 삭제할 수 있습니다.');
                    window.location.href="/board-home";
                }
            })
            .catch(error => console.error('There has been a problem with your fetch operation:', error));
        });
    }

});


</script>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="../../js/contact/showPost.js"></script>

</body>
</html>

이 전체코드가 필요한 건 아니고, 현재 2단계 : 추천 누르는 기능 구현함에 있어서 필요한 부분만 추리자면 아래와 같다. 

<div id="thumb-up-div">
    <span>
    	<i class="fa-solid fa-thumbs-up"></i> &nbsp 추천
    </span> 
    <span id="like-number">
    	${likeNumber}
    </span> 
</div>

 

이 부분과 , fetch 하는 부분 중,  아래부분이다. 

document.addEventListener("DOMContentLoaded", function(){
    $("#thumb-up-div>span:nth-child(1)").click(function() {
        fetch('/update-and-find-like-num', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            postId: ${findPost.id},
          }),
        })
        .then(response => {
            if(!response.ok){
                throw new Error('Network response not ok');
            }
            console.log('여기 문제있음2');
            return response.json();
        })
        .then(data => {
            if(data.updatedLikeNumber == -1){
                alert('좋아요 는 1번만 누르실 수 있습니다');
            }else{
                // 여기서 가져온 updatedLikeNumber 를 출력한다.
                document.querySelector('#like-number').innerText = data.updatedLikeNumber;
            }
        })
        .catch(error => console.error('There has been a problem with your fetch operation:', error));
    })
});

fetch 구문을 보면, 

이 부분에서, 추천버튼을 누르면, 이 게시글의 posts 테이블 id 컬럼값과 함께 localhost:8080/update-and-find-like-num 이라는 url 로 post 요청을 보내고 있다. 이를 매핑하는 컨트롤러는 아래와 같이 생겼다. 

    @PostMapping("update-and-find-like-num")
    @ResponseBody // 이 어노테이션 추가
    public Map<String, Integer> updateAndFindLikeNum(@RequestBody Map<String, String> map, HttpServletRequest request) {
        String postId = map.get("postId");
        int pi = Integer.parseInt(postId); // 현재 pi 에는 추천을 누른 해당 게시글의 posts 테이블의 id 컬럼값이 들어있다.

        Integer id = FindLoginedMemberIdUtil.findLoginedMember(request); // 현재 id 에는 추천을 누른 사용자의 member 테이블 id 컬럼값이 들어있다.

        // 지금 추천을 누른 사용자가 해당 게시글에 추천을 누른적이 있는지를 확인하기 위해서,
        // POST_ID_MEMBER_ID_FOR_NOT_DUPLICATE_LIKE 라는 테이블에서 pi 와 id 로 조회를 하고 있다.
        Integer rowCount = postIdMemberIdForNotDuplicateLikeMapper.findRowCountByPostIdAndMemberId(pi, id);
        Map<String, Integer> response = new HashMap<>();

        if(rowCount == 0){
            // POST_ID_MEMBER_ID_FOR_NOT_DUPLICATE_LIKE 테이블에서 조회된 행이 없다
            // == 지금 추천 누른 사람이 이 글에 좋아요를 처음 누른 경우.

            // 뭘 해줄거냐면, POST_LIKE 테이블에 행을 삽입해줘야 함.
            // 게시글을 만드는 과정에서도, 그리고 사용자가 게시판 홈에서 제목을 클릭해서 게시글의 상세조회를 보여주는 과정에서도,
            // POST_LIKE 테이블에 행을 넣은 적이 없음.
            // 1번. pi 로 POST_LIKE 테이블에서 like_number 컬럼값 을 조회한다.
            // 2번. 만약, 조회된 값이 null이라면(= 조회된 행이 없다면), 해당 글에 대해 최초로 추천(좋아요)가 눌린 상황이다. 따라서, POST_LIKE 테이블에 행을 삽입해주고, like_number 컬럼값을 + 1 해줘야함.
            // 3번. 만약, 조회된 값이 null이 아니라면(= 조회된 행이 있다면), 이미 위의 과정을 거쳤다고 판단하고, like_number 컬럼값만 + 1 해주면 됨.
            Integer likeNumber = postLikeMapper.findPostLikeCountByPostId(pi);

            if (likeNumber == null) {
                //조회된 컬럼값이 비어있다면, 해당 글에 대해 최초로 추천(좋아요)이 눌린 상황.
                Integer sequence = postLikeMapper.selectNextSequenceValue(); //시퀀스 미리 가져오고,
                postLikeMapper.initPostLike(pi); // 행을 삽입.
            }
            // 2번의 상황이든, 3번의 상황이든, 공통적으로 like_num 을 +1 해줘야함.
            postLikeMapper.updateByPostId(pi);
            // 2번의 상황이든, 3번의 상황이든, 공통적으로 post_id_member_id_for_not_duplicate_like 테이블에 행을 넣어줘야 함.
            postIdMemberIdForNotDuplicateLikeMapper.insertRow(pi, id);

            // 다시 조회. 왜? likeNumber 에 들어있는 값은, like_number 컬럼을 +1 해주기 이전의 값이기 때문.
            Integer updatedNumber = postLikeMapper.findPostLikeCountByPostId(pi);
            
            response.put("updatedLikeNumber", updatedNumber);
            return response; // JSON 형식의 맵 반환
        } else{
            // 조회된 행이 1 이상인 경우
            // == 지금 추천을 누른 사람이 이 글에 추천(좋아요)을 이전에 누른 경우.
            // == 이미 POST_LIKE 테이블에 행이 있다는 것도 의미하는 것이다.
            response.put("updatedLikeNumber", -1); //jsp 파일에서 1이면 alert 창으로 "좋아요 는 1번만 누르실 수 있습니다" 라는 문구 띄워줘.
            return response;
        }

    }

전체적인 흐름에 대해서 정리해두자면, 

POST_ID_MEMBER_ID_FOR_NOT_DUPLICATE_LIKE 라는 테이블에 대한 설명을 하는 것에서부터 시작해야 한다. 

어떤 사용자가 추천(좋아요) 를 누른 순간, POST_ID_MEMBER_ID_FOR_NOT_DUPLICATE_LIKE 라는 테이블에 그 게시물의 POSTS 테이블 id 컬럼과 그걸 누른 사용자의 MEMBER 테이블 id 컬럼을 저장하는 테이블이다. 

그래서, 해당 게시글에 대해서 동일한 사용자가 추천(좋아요)를 다시 한번 누르는 경우, POST_ID_MEMBER_ID_FOR_NOT_DUPLICATE_LIKE 라는 테이블에서 그 사용자의 member테이블 id 컬럼과 추천(좋아요)를 누른 게시글의 POSTS 테이블 id 컬럼값으로 조회되는 행이 있는지를 보는것이다.

있다 -> 이미 그 게시글에 그 사용자가 추천(좋아요)를 눌렀다 -> 좋아요 수 + 1 안해줌.

없다 -> 그 사용자가 그 게시글에 처음 추천(좋아요)를 눌렀다 -> 좋아요 수 + 1 해줌.

 

이런 원리로 돌아가는 거고, 그래서, 이 컨트롤러가 updatedLikeNumber 라는 키에 -1을 담아줬으면, fetch 의 then 에서 alert() 로 좋아요는 1번만 누를 수 있습니다. 라고 안내해주는것이다.

그리고, -1 이 아니라, 그 게시글의 업데이트된 좋아요수를 리턴했다면, 

document.querySelector('#like-number').innerText = data.updatedLikeNumber;

이 코드로 

 

이 부분에 업데이트된 좋아요 수를 렌더링해주면 된다. 

 

끝. 

다음 포스팅은 게시판 홈에서 아래와 같이 좋아요순으로 게시글을 조회하기, 조회수순으로 게시글을 조회하기 두 기능을 구현해볼 것이다. 

 

그리고, 게시판 홈에서, 아래와 같이, 제목 또는 작성자 또는 내용으로 게시물을 조회할 수 있도록 해두었는데, 이를 구현해볼 것이다.