토이 프로젝트/익명 채팅 사이트

프로젝트 진행 ⑧ : 댓글 상세보기 페이지, 댓글 더보기 기능 구현

OhPro 2022. 8. 2. 21:58
반응형

목차

  • 시작하며
  • 댓글 상세보기 페이지 구현 방식
  • 댓글 더보기 기능 구현 방식
  • 마치며

 


 

시작하며

이번에는 댓글 상세보기 페이지와 댓글 더보기 기능을 구현한 것에 대해 어떤 방식으로 구현했는지 포스팅하려고 한다.

 

이렇게 댓글 상세보기 페이지를 따로 만든 이유는 다음과 같다.

 

  1. 제일 처음에 와이어 프레임을 설계할 때 댓글 상세보기 페이지를 만들 계획을 했다. (설계대로 만들기..)
  2. 제일 적은 비용으로 서비스를 만들려면 댓글에 추가적인 버튼을 만들어 JS의 XmlHttpRequest로 조금씩 데이터를 받아오는 것이 좋다고 생각했으나, 이틀 정도를 고민했음에도 JS에서 Servlet으로부터 받아온 한글 데이터가 깨지는 현상을 고치지 못했다. 

 

그래서 기존 설계대로 댓글 상세보기 페이지를 만들기로 했다!

 


 

댓글 상세보기 페이지 구현

 

댓글 상세보기 페이지 UI

 

댓글 상세보기 페이지 UI는 위와 같은 형태로 작성하였는데, 만들면서 신경 썼던 포인트는

 

  1. 해당 댓글이 상단에 크게 디스플레이되어야 한다는 것.
  2. 본래 글로 돌아갈 수 있어야 한다는 것.
  3. 신고가 가능해야 한다는 것.
  4. 댓글 작성에 대한 정보(닉네임, 날짜)가 디스플레이 되어야 한다는 것.
  5. 대댓글을 익명으로 작성할 수 있어야 한다는 것.

 

으로 일단 생각했던 기능은 구현되었다고 판단하고 있다.

 

 

JSP를 살펴보면,

 

JSP 3 - 댓글 본문

 

ul, li 태그는 본래 목록을 만들 때 사용하는 태그들이지만, 여기서는 부트스트랩 디자인을 사용하기 위해 적절히 사용하고 있다.

 

중간에 EL을 사용하여 ${comment.content}와 같이 댓글 내용을 반환해주도록 했다. 컨트롤러에서 댓글 상세보기 페이지 요청을 처리하는 과정에서 Comment 객체를 통으로 넘겨주기 때문에 Comment 객체의 멤버인 content를 불러올 수 있는 것이다.

 

그리고 만약 commnet 객체의 멤버 중 isUseAnonymousName 속성이 true라면 닉네임에 익명을 표기할 수 있도록 했다.

 

하지만 이 부분은 수정하려고 계획 중이고 이유는 다음과 같다.

 

  1. 애초에 컨트롤러에서 요청받은 댓글이 익명으로 게시된 댓글이라면 comment.nicname 에 "익명"이라는 값을 넣어서 응답해줘도 되는 부분이다.
  2. 해당 댓글 작성자가 게시할 때 익명으로 게시하려고 했다면, 댓글 게시 요청을 처리하는 컨트롤러에서 comment 객체를 DB에 저장할 때부터 "익명" 값으로 저장하도록 하면 굳이 익명 여부를 체크할 필요가 없기 때문이다.

 

여기서 comment객체는 DB에 저장하는 객체로 entity객체이고, 이전 설계 과정에서 정규화를 하려고 시도까지 했었기에 원래는 nicname이라는 멤버(or 어트리뷰트)는 comment 객체의 멤버가 아니었다.

 

하지만 쿼리를 두 번 실행하는 것에 대해 비효율적이라는 생각을 하면서 작업을 하다 보니 작성자 닉네임을 테이블에 갖고 있어야 좋을 것 같아 nicname을 comment에 추가한 것이다.

 

지금은 쿼리를 두 번 날리더라도 "작업을 진행시켜놓고 성능 개선이 필요하면 그때 추가해도 되지 않을까"라는 생각도 있지만 동시에 뭔가 모든 작업이 완수되었는데 성능 개선을 위해 이 작업을 해야 한다면 그때 가서 미리 nicname컬럼을 추가하지 않은 것에 대해 후회할 것 같은 생각도 가지고 있다. (그런 이유에서 추가한 것이다..^^)

 

 

어찌 되었든 어떻게 수정할 것인가가 중요하므로 현재 나의 생각은 이렇다.

 

comment 객체의 멤버 중에는 accountNo(=계정 번호)가 존재하기 때문에
nicname컬럼에는 익명이 작성되어도 상관없으므로

댓글을 익명으로 게시하면 DB에 저장할 때부터

 comment테이블의 nicname컬럼에는 "익명"이 저장되도록 구성할 것.

 

나머지로는 댓글이 작성된 날짜를 디스플레이하기 위해 EL을 사용하여 ${comment.commentedDate}를 사용한 것이 있다.

 

JSP 2 - 대댓글 조회

 

대댓글도 댓글과 마찬가지로 익명으로 게시할 수 있도록 했기 때문에 JSTL의 c:if를 사용하여 이름 표시 조건을 걸어두었다. 대댓글도 댓글과 마찬가지로 nicname 컬럼을 추가했고 이후에 nicname 컬럼 자체에 "익명"을 저장하도록 바꾸어 c:if를 최대한 자제하려고 한다.

 

또한 이렇게 함으로써 익명 처리 여부에 대한 조건이 잘못 처리되면 익명으로 게시한 댓글, 대댓글에 유저 닉네임이 디스플레이될 수도 있는 문제를 미연에 방지할 수 있을 것 같다.

 

 

JSP 3 - 대댓글 입력 폼

 

대댓글 목록의 하단에 대댓글을 입력할 수 있도록 폼을 만들었고 코드 설명은 다음과 같다.

 

  1. form 태그를 최상위로 자식 태그 중 첫 번째로 익명으로 게시할지 여부를 설정할 수 있는 체크박스를 두었다.
  2. form 태그의 두 번째 자식 태그로 입력 그룹을 만들었는데 대댓글 본문과 버튼에 덧붙여 hidden parameter로 댓글 번호, 대댓글 작성자 계정 번호를 송신할 수 있도록 만들었다.

 

 

이제 컨트롤러의 코드를 살펴보자. 가장 먼저 댓글 상세보기 요청에 대한 컨트롤러 메서드는 아래와 같다.

 

    @RequestMapping(uri = "/comment", method = "get")
    public ViewInfo showCommentPage(HttpServletRequest req, HttpServletResponse resp){
        String no = req.getParameter("no");

        Comment comment = postService.getComment(no);
        if (Comment.isCorrectComment(comment))
            return ViewInfo.getRedirectViewInfo("/post?no="+no);

        Post post = postService.getPost(Integer.toString(comment.getPostNo()));
        String title = post.getTitle();
        if (title.length() > 7)
            title = title.substring(0,7) + "...";

        List<Reply> replies = postService.getReplies(comment);
        req.setAttribute(COMMENT, comment);
        req.setAttribute(REPLIES, replies);
        req.setAttribute(PARENT_TITLE, title);

        return new ViewInfo("commentDetailPage");
    }

 

  1. @RequestMapping (스프링 어노테이션 아니라 직접 만든 어노테이션입니다^^)에서 "/comment"라는 URL로 "get"메서드를 사용해 들어온 요청을 처리하는 메서드로 만들었다.
  2. 요청을 처리하여 comment를 DB에서 획득하기 위해 req 객체에서 댓글 번호를 획득하기 위해 필요한 "no"(번호라는 뜻)를 받아왔다.
  3. PostServicegetComment(String commentNo) 메서드를 사용하여 DB에 저장되어있던 comment를 한 건 가져온다.
  4. static 메서드인 Comment.isCorrectComment(Comment comment)를 사용하여 받아온 comment에 값이 올바르게 들어가 있는지 체크한다.
  5. PostServicegetPost(String postNo), getReplies(Comment comment) 메서드를 사용하여 해당 댓글의 부모인 글과 댓글에 작성된 대댓글 리스트를 불러온다.
  6. 글 제목이 길 경우에는 중간에 말 줄임표를 만들어 적당한 사이즈의 길이를 가질 수 있도록 했고, String 변수인 title에 말 줄임표 작업이 실시된 텍스트를 넣어준다.
  7. 최종적으로 JSP에서 사용할 객체들을 req.setAttribute(key, object) 메서드를 사용하여 넣어준다.
  8. ViewInfo 객체를 사용하여 댓글 상세보기 페이지를 반환해준다.

 

    @RequestMapping(uri = "/reply", method = "post")
    public ViewInfo createReply(HttpServletRequest req, HttpServletResponse resp){
        String content = req.getParameter("content");
        String replierNo = req.getParameter("accountNo");
        String commentNo = req.getParameter("commentNo");
        String isAnonName = req.getParameter("isUseAnonymousName");
        postService.createReply(content, replierNo, commentNo, isAnonName);
        return ViewInfo.getRedirectViewInfo("/comment?no="+commentNo);
    }

 

  1. @RequestMapping 어노테이션으로 uri가 "/reply"이고, 요청한 method가 "post"인 요청을 처리하는 메서드로 설정했다.
  2. 대댓글 내용, 대댓글 작성자 번호, 댓글 번호, 익명 여부를 req.getParameter(String key) 메서드를 사용해서 받아온다.
  3. postService(서비스 객체)의 createReply(String content, String replierNo, String commentNo, String isAnonName) 메서드를 사용하여 받아온 정보들을 토대로 생성한다.

 

    public void createReply(String content, String replierNo, String commentNo, String isAnonName) {
        int accountNo = -1;
        int commentNoInt = -1;
        try {
            accountNo = Integer.parseInt(replierNo);
            commentNoInt = Integer.parseInt(commentNo);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        boolean isAnonymousName = true;
        if (isAnonName == null)
            isAnonymousName = false;

        Profile profile = profileDao.getProfile(accountNo);
        if (profile == null) return;

        replyDao.createReply(content, profile, commentNoInt, isAnonymousName);
    }

 

일단 최대한 컨트롤러 코드가 더러워지지 않도록 매개변수들을 날 것(?)의 형태인 String 타입으로 받아와서 int 형태로 변형해주었다.

 

try catch문을 사용해서 int형으로 변환하는 과정에서 Exception이 발생해도 오류가 발생하지 않고 단지 대댓글이 추가되지 않도록 했다. 만약 여기서 Exception이 발생할 시 처리과정은 다음과 같다.

 

  1. 익셉션이 발생하면서 컨트롤러 메서드가 비정상 종료될 것이고, adapter 객체invoke 과정 중 Exception이 발생했으므로 뷰 정보를 담고 있는 객체가 정상적으로 생성되지 않는다. (adapter도 직접 만든 객체로 여기를 클릭하면 어떻게 만들었는지 볼 수 있다.)
  2. 뷰 객체를 생성할 수 없거나 해당 뷰 객체가 담고 있는 뷰 이름이 존재하지 않는 뷰 페이지(JSP)였을 경우 adapter 객체에서 pageNotFound를 반환하게 된다.
  3. pageNotFound는 페이지를 찾지 못했다는 의미의 뷰 페이지 이름으로 해당 페이지가 반환되면 "페이지를 찾지 못했습니다."라는 문구밖에 없는 페이지가 반환된다.
  4. 결론적으로 유저는 단지 대댓글을 게시하려고 했을 뿐인데 "페이지를 찾지 못했습니다"라는 글귀를 보게 되는 것이다.

 

그래서 항상 String 형태의 변수를 int 형태로 바꿀 때는 try ~ catch 문을 사용해서 이런 오류들을 방지해두고 있다.

 

    public void createReply(String content, Profile profile, int commentNoInt, boolean isAnonName) {
        String sql = "insert into reply " +
                "(reply_no, content, is_use_anonymous_name, account_no, comment_no, nicname, replied_date)" +
                "values " +
                "(null, ?, ?, ?, ?, ?, now())";
        try (
                Connection conn = new DBUtil().getConnection();
                PreparedStatement preparedStatement = conn.prepareStatement(sql);
        ) {
            preparedStatement.setString(1, content);
            preparedStatement.setBoolean(2, isAnonName);
            preparedStatement.setInt(3, profile.getAccountNo());
            preparedStatement.setInt(4, commentNoInt);
            preparedStatement.setString(5, profile.getNicname());
            preparedStatement.execute();

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

 

컨트롤러로부터 받은 변수들을 정상적으로 형 변환까지 마쳤다면 Dao객체로 해당 변수들을 넘겨주어 DB에 Insert 될 수 있도록 해주었다.

 

현재는 JDBC를 사용하여 MySQL에 접근하도록 하고 있기 때문에 위와 같은 형태로 코딩했다.

 


 

댓글 더보기 기능 구현

댓글 더 보기 버튼

 

원래는 댓글들을 JSTL의 c:foreach 태그를 사용해서 한 번에 보여주려고 했지만, 뭔가 댓글이 많거나 하면 좀 보기 힘들고 댓글을 작성하기도 힘들 거 같아서 JS를 사용해서 더보기 버튼을 클릭하면 5개씩 점차적으로 보여주는 방식으로 변경했다.

 

<!-- 댓글 -->
<div class="">
    <ul id="ul-comments" class="list-group list-group-flush">
        <c:forEach var="comment" items="${comments}">
            <li class="list-group-item">
                <span class="commenter-nicname">
                    <c:if test="${comment.isUseAnonymousName ne true}">
                        ${comment.nicname}
                    </c:if>
                    <c:if test="${comment.isUseAnonymousName eq true}">
                        익명
                    </c:if>
                </span><br>
                <span class="comment-content"> ${comment.content} </span>
                <button id="btn-expand-reply" class="btn btn-sm" onclick="location.href='/comment?no=${comment.commentNo}'">➥</button>
            </li>
        </c:forEach>
        <button type="button" class="btn btn-dark" id="btn-more-comment">더 보기</button>
    </ul>
    <form style="margin-top: 2%" action="${entryComment}" method="post">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" id="isAnonymous" name="isUseAnonymousName">
            <label class="form-check-label" for="isAnonymous">
                익명으로 게시하기
            </label>
        </div>
        <div class="input-group text-center">
            <input type="hidden" name="postNo" value="${post.postNo}">
            <input type="hidden" name="accountNo" value="${sessionScope.get("profile").accountNo}">
            <input type="text" class="form-control" placeholder="댓글을 입력해주세요!" name="content" >
            <button class="btn btn-outline-secondary" type="submit" id="btn-comment-submit">등록</button>
        </div>
    </form>
</div>
<!-- 댓글 -->

 

위 JSP 코드를 기준으로 18번째 줄에 있는 더보기 버튼을 추가했고, JS는 아래와 같이 작성했다.

 

const btnMoreComment = document.getElementById("btn-more-comment");

btnMoreComment.addEventListener("click", showMoreComment);

window.onload = (() => {
    localStorage.setItem("commentCount", "0");
    showMoreComment();
});

function showMoreComment(){
    const lcValue = localStorage.getItem("commentCount");
    console.log(lcValue);
    let commentCount = parseInt(lcValue);
    if (commentCount === 0) {
        commentCount = 5;
    } else {
        commentCount = commentCount + 5;
    }

    localStorage.setItem("commentCount", JSON.stringify(commentCount));

    const ul = document.getElementById("ul-comments");
    const lis = ul.getElementsByTagName("li");

    for (let i = lis.length - 1; i >= 0; i--) {
        if (i>=commentCount) {
            lis[i].style.display = "none";
        } else {
            lis[i].style.display = "";
        }
    }
}

 

코드를 설명하자면

 

  • 맨 처음에 "btn-more-comment"라는 id를 가진 엘리먼트를 불러와 btnMoreComment라는 변수에 저장하는데, 해당 엘리먼트는 위 JSP에서 더보기 버튼에 해당한다.
  • addEventListener() 함수를 사용하여 btnMoreComment버튼에 클릭 시 showMoreComment() 메서드를 발동시킬 수 있도록 했다.

 

 

그 후 로컬 스토리지를 사용해야 하여 디스플레이할 댓글의 개수를 임시 저장하는 방식을 사용하려고 하는데, 만약 해당 글의 조회를 마치고 다른 글로 페이지 이동을 했을 때 로컬 스토리지 값이 초기화되지 않으면 기능 수행이 제대로 되지 않으므로 아래와 같이 조치했다.

 

  • window.onloadarrow function을 넣었다.
  • arrow function에서는 로컬 스토리지에 저장될 댓글 개수를 5개로 초기화하고 showMoreComment() 함수를 1회 수행하도록 설정해두었다.

 

 

이제 실제 더 보기 기능을 수행할 함수인 showMoreComment() 함수를 살펴보자

 

  • 먼저 로컬 스토리지에 저장된 "commentCount"를 키 값으로 가진 값을 불러온다.
  • 만약 commentCount 값이 없었다면 5로 설정해주고, 존재한다면 기존 값에 +5 해줬다.
  • +5 작업을 마친 값을 "commentCount"에 다시 저장해준다.
  • 댓글 목록을 가지고 있는 ul 태그를 id로 불러온 후, 자식 태그 중 "li"를 태그로 갖고 있는 배열을 getElementByTagName() 메서드를 사용하여 불러온다.
  • lis 배열의 마지막에서부터 commentCount에 저장된 값의 순서까지는 style에 display를 none으로 만들어 보이지 않도록 해줬다.

 

 

로컬 스토리지 값

 

위 사진은 더보기 버튼을 1 회 클릭했을 때로 정상적으로 10으로 변경된 모습을 캡처한 것이다.

 

 


 

마치며

지금 느낌이 코딩을 처음 배울 때처럼 나름 계획도 하고 시작한 프로젝트이긴 한데 하면서 계속 배우고 있다. 뭔가 이런 식으로 한 번 끝내면 이다음 프로젝트부터는 더 분석적으로 꼼꼼하게 할 수 있을 것 같은 기분이 든다^^.

 

 

감사합니다!!

반응형