Bigfat

[Web] Ajax를 이용한 댓글 쓰기 구현하기 (게시판 구현) 본문

Java/web

[Web] Ajax를 이용한 댓글 쓰기 구현하기 (게시판 구현)

kyou 2017. 4. 13. 16:20

Ajax를 이용해 비동기 통신으로 댓글 쓰기를 구현해보자

  기존 웹 애플리케이션은 브라우저에서 채운 form을 웹 서버로 제출(submit)하는 요청으로 웹 서버의 중복되는 HTML 코드의 전송으로 대역폭의 낭비를 야기할 수 있다. Ajax(Asynchronous JavaSript and XML, 에이잭스)는 페이지 이동 없이 필요한 데이터만을 웹서버에 요청해서 받은 후 클라이언트에서 데이터에 대한 처리를 할 수 있는 기술이기 때문에 기존 웹 애플리케이션의 단점을 극복할 수 있다. 

  웹 서버와 비동기적으로 데이터를 교환하고 조작하기 위해 XML을 이용했기 때문에 붙여진 이름이지만, Ajax 애플리케이션은 XML 대신하는 데이터 포맷으로 JSON(JavaScript Object Notation)을 이용할 수 있다.

  프로젝트 구조는 아래와 같이 진행된다. com.edu.comment패키지를 생성해 기존 패키지와 분리하였다. JSON 라이브러리를 사용하기 위해 메이블 리퍼지토리에서 다운로드 받은 후 /WEB-INF/lib에 추가한다.


  댓글(comment)에 관한 테이블과 시퀀스를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--댓글 테이블 생성
CREATE TABLE comments(
  comment_number NUMBER PRIMARY KEY,
  id VARCHAR2(15NOT NULL,
  comment_content VARCHAR2(200NOT NULL,
  comment_date DATE NOT NULL,
  article_number NUMBER NOT NULL,
  --글이 지워지면 해당 답글도 지워져야 한다.
  --article_number > delete option : cascade
CONSTRAINT comment_fk FOREIGN KEY(article_number)
REFERENCES bbs(article_number) ON DELETE CASCADE);
 
--댓글 시퀀스 생성
CREATE SEQUENCE comment_seq
 START WITH 1
INCREMENT BY 1;
cs


  bbs.properties파일에 /commentWrite.bbs, /commentRead.bbs를 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#bbs.properties
/writeForm.bbs=com.edu.bbs.WriteFormImpl
/write.bbs=com.edu.bbs.WriteImpl
/list.bbs=com.edu.bbs.ListImpl
/content.bbs=com.edu.bbs.ContentImpl
/login.bbs=com.edu.bbs.LoginImpl
/logout.bbs=com.edu.bbs.LogoutImpl
/updateForm.bbs=com.edu.bbs.UpdateFormImpl
/update.bbs=com.edu.bbs.UpdateImpl
/delete.bbs=com.edu.bbs.DeleteImpl
/replyForm.bbs=com.edu.bbs.ReplyFormImpl
/reply.bbs=com.edu.bbs.ReplyImpl
/commentWrite.bbs=com.edu.comment.CommentWriteImpl
/commentRead.bbs=com.edu.comment.CommentReadImpl

cs


  댓글 테이블의 데이터를 다루기 위해 CommentDto클래스를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.edu.comment;
 
public class CommentDto {
    private int commentNumber;
    private String id;
    private String commentContent;
    private String commentDate;
    private int articleNumber;
    
    public int getCommentNumber() {
        return commentNumber;
    }
    public void setCommentNumber(int commentNumber) {
        this.commentNumber = commentNumber;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getCommentContent() {
        return commentContent;
    }
    public void setCommentContent(String commentContent) {
        this.commentContent = commentContent;
    }
    public String getCommentDate() {
        return commentDate;
    }
    public void setCommentDate(String commentDate) {
        this.commentDate = commentDate;
    }
    public int getArticleNumber() {
        return articleNumber;
    }
    public void setArticleNumber(int articleNumber) {
        this.articleNumber = articleNumber;
    }
    
    @Override
    public String toString() {
        return "CommentDto [commentNumber=" + commentNumber + ", id=" + id + ", commentContent=" + commentContent
                + ", commentDate=" + commentDate + ", articleNumber=" + articleNumber + "]";
    }
    
}
cs


  BBSDto클래스에 commentCount 멤버 변수를 선언해주고, getter, setter메서드를 추가하고, 오버라이딩한 toString메서드에 commentCount가 추가될 수 있도록 변경해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.edu.bbs;
 
import java.sql.Timestamp;
 
public class BBSDto {
 
    private int articleNumber;
    private String id;
    private String title;
    private String content;
    private int groupId;
    private int depth;
    private int pos;
    private int hit;
    private Timestamp writeDate;
    private String fileName;
    private long commentCount;            // 추가한다.
    
    public int getArticleNumber() {
        return articleNumber;
    }
    public void setArticleNumber(int articleNumber) {
        this.articleNumber = articleNumber;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public int getGroupId() {
        return groupId;
    }
    public void setGroupId(int groupId) {
        this.groupId = groupId;
    }
    public int getDepth() {
        return depth;
    }
    public void setDepth(int depth) {
        this.depth = depth;
    }
    public int getPos() {
        return pos;
    }
    public void setPos(int pos) {
        this.pos = pos;
    }
    public int getHit() {
        return hit;
    }
    public void setHit(int hit) {
        this.hit = hit;
    }
    public Timestamp getWriteDate() {
        return writeDate;
    }
    public void setWriteDate(Timestamp writeDate) {
        this.writeDate = writeDate;
    }
    public String getFileName() {
        return fileName;
    }
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
    public long getCommentCount() {
        return commentCount;
    }
    public void setCommentCount(long commentCount) {
        this.commentCount = commentCount;
    }
    
    @Override
    public String toString() {
        return "BBSDto [articleNumber=" + articleNumber + ", id=" + id + ", title=" + title + ", content=" + content
                + ", groupId=" + groupId + ", depth=" + depth + ", pos=" + pos + ", hit=" + hit + ", writeDate="
                + writeDate + ", fileName=" + fileName + ", commentCount=" + commentCount + "]";
    }
 
}
cs


  BBSOracleDao클래스에 comment, comments 멤버 변수를 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BBSOracleDao implements LoginStatus {
 
    private static BBSOracleDao bbsOracleDao = new BBSOracleDao();
    private OracleDBConnector orclDbc = OracleDBConnector.getInstacne();
    
    Connection conn;
    PreparedStatement pstmt;
    ResultSet rs;
    StringBuffer query;
    BBSDto article;
    ArrayList<BBSDto> articleList;
    int totalCount;
 
    // 멤버 변수를 추가한다.
    CommentDto comment;
    ArrayList<CommentDto> comments;
 
    ...
cs


  BBSOracleDao클래스의 selectArticle메서드에 해당 글의 comment 총 개수를 조회하는 코드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public BBSDto selectArticle(String articleNumber) throws ClassNotFoundException, SQLException {
    conn = orclDbc.getConnection();
    query = new StringBuffer();
    query.append("SELECT * FROM bbs WHERE article_number = ?");
    pstmt = conn.prepareStatement(query.toString());
    pstmt.setString(1, articleNumber);
    rs = pstmt.executeQuery();
    
    if(rs.next()) {
        article = new BBSDto();
        article.setArticleNumber(rs.getInt("article_number"));
        article.setId(rs.getString("id"));
        article.setTitle(rs.getString("title"));            
        article.setDepth(rs.getInt("depth"));            
        article.setContent(rs.getString("content"));
        article.setHit(rs.getInt("hit"));
        article.setGroupId(rs.getInt("group_id"));
        article.setPos(rs.getInt("pos"));
        article.setWriteDate(rs.getTimestamp("write_date"));
        article.setFileName(rs.getString("file_name"));
    }
    
    // Comment Counting
    query = new StringBuffer();
    query.append("SELECT count(*) FROM comments WHERE article_number = ?");
    pstmt = conn.prepareStatement(query.toString());
    pstmt.setString(1, articleNumber);
    rs = pstmt.executeQuery();
    
    if(rs.next()) {
        article.setCommentCount(rs.getLong(1));
    }
    
    disconnect();
    
    return article;
}
cs


  BBSOracleDao클래스에 아래 메서드를 추가한다. insertComment메서드는 return값을 JSONObject의 생성자 파라미터로 넣어주기 위해 타입을 HashMap으로 선언하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * BBSOracleDao클래스에 추가한다.
 * @param articleNumber
 * @param commPageSize
 * @return
 */
public ArrayList<CommentDto> selectComments(String articleNumber, int commPageSize) throws ClassNotFoundException, SQLException {
    conn = orclDbc.getConnection();
    query = new StringBuffer();
    query.append("SELECT * ");
    query.append("  FROM (SELECT id, comment_content, comment_date, article_number ");
    query.append("               FROM comments ");
    query.append("             WHERE article_number = ? ");
    query.append("             ORDER BY comment_number DESC");
    query.append("           ) comments ");
    query.append(" WHERE rownum BETWEEN 1 AND ?");
    pstmt = conn.prepareStatement(query.toString());
    pstmt.setString(1, articleNumber);
    pstmt.setInt(2, commPageSize);
    rs = pstmt.executeQuery();
    
    comments = new ArrayList<>();
    
    while(rs.next()) {
        comment = new CommentDto();
        comment.setId(rs.getString("id"));
        comment.setCommentContent(rs.getString("comment_content"));
        comment.setCommentDate(rs.getString("comment_date"));
        comment.setArticleNumber(rs.getInt("article_number"));
        comments.add(comment);
    }
    
    disconnect();
    
    return comments;
}
 
public synchronized HashMap<String, Object> insertComment(String id, String commentContent, String articleNumber) throws ClassNotFoundException, SQLException {
    conn = orclDbc.getConnection();
    pstmt = conn.prepareStatement("INSERT INTO comments VALUES(comment_seq.nextval, ?, ?, sysdate, ?)");
    pstmt.setString(1, id);
    pstmt.setString(2, commentContent);
    pstmt.setString(3, articleNumber);
    int result = pstmt.executeUpdate();
    ArrayList<CommentDto> comments = selectComments(articleNumber, 10);
    
    HashMap<String, Object> hm = new HashMap<>();
    hm.put("result", result);
    hm.put("comments", comments);
    
    disconnect();
    
    return hm;
}
cs


  /commentWrite.bbs 요청을 처리할 CommentWriteImpl클래스를 아래와 같이 작성한다. resp의 getWriter메서드로 PrintWriter객체를 얻어 JSONObject를 output한다. getWriter메서드는 HttpServletResponse의 부모인 ServletResponse인터페이스에 선언되어있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.edu.comment;
 
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.json.JSONObject;
 
import com.edu.bbs.BBSOracleDao;
import com.edu.bbs.BBSService;
 
public class CommentWriteImpl implements BBSService {
 
    @Override
    public String bbsService(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");
        
        String id = req.getSession().getAttribute("id").toString();
        String commentContent = req.getParameter("commentContent");
        String articleNumber = req.getParameter("articleNumber");
        HashMap<String, Object> result = null;
        
        try {
            result = BBSOracleDao.getInstance().insertComment(id, commentContent, articleNumber);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        JSONObject jsonObj = new JSONObject(result);
        PrintWriter pw = resp.getWriter();
        pw.println(jsonObj);
        
        return null;
    }
 
}
cs


  /commentRead.bbs 요청을 처리할 CommentRead클래스를 아래와 같이 작성한다. JSONArray객체의 생성자 파라미터로 selectComments메서드의 return값인 ArrayList<CommentDto>를 넣어주고, 위와 동일하게 output한다. 오버라이딩한 메서드명이 잘못 작성되어있어 bbs()에서 bbsService()로 변경하였고, 패키지 구조를 프로젝트에 맞게 com.edu.comment로 변경하였습니다(17.05.29.). 이클립스나 STS와 같은 IDE에서는 Refactor를 이용해 파일명을 변경하거나 파일을 이동시키도록 합시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.edu.comment;
 
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.json.JSONArray;
 
import com.pknu.bbs.BBSOracleDao;
import com.pknu.bbs.BBSService;
 
public class CommentReadImpl implements BBSService {
 
    @Override
    public String bbsService(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        resp.setContentType("text/html;charset=utf-8");
        resp.setCharacterEncoding("utf-8");        // JSON 한글 깨짐 해결
        
        int commPageNum = Integer.parseInt(req.getParameter("commPageNum"));
        String articleNumber = req.getParameter("articleNumber");
        ArrayList<CommentDto> comments = null;
        
        try {
            comments = BBSOracleDao.getInstance().selectComments(articleNumber, commPageNum);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        JSONArray jsonArr = new JSONArray(comments);        // 스프링에선 애노테이션(?)
        PrintWriter pw = resp.getWriter();
        pw.println(jsonArr);
        
        return null;
    }
 
}


cs


  content.jsp에 comment를 추가해줄 부분에 아래 코드를 삽입한다. 댓글 입력 textarea 코드가 누락되어 추가했습니다(17.05.29.).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="input-group" role="group" aria-label="..." style="margin-top: 10px; width: 100%;">
    <textarea class="form-control" rows="3" id="commentContent" placeholder="댓글을 입력하세요." style="width: 100%;" ></textarea>
    <div class="btn-group btn-group-sm" role="group" aria-label="...">
        <c:if test="${id == null}">
            <input type="button" class="btn btn-default" value="댓글 쓰기" disabled="disabled">
        </c:if>
        <c:if test="${id != null}">
            <input type="button" class="btn btn-default" value="댓글 쓰기" id="commentWrite">
        </c:if>
        <input type="button" class="btn btn-default" value="댓글 읽기(${article.commentCount})" 
                onclick="getComment(1, event)" id="commentRead">
    </div>
</div>
 
<!-- Comment 태그 추가 -->
<div class="input-group" role="group" aria-label="..." style="margin-top: 10px; width: 100%;">
    <div id="showComment" style="text-align: center;"></div>
</div>
cs


  content.jsp에 아래 스크립트를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<script>
    jQuery(document).ready(function() {
        if(${id== null}) {
            alert("게시판을 이용하시려면 로그인하셔야 합니다.");
            location.href="/bbs/login.bbs";
        }
    });
    
    // Perform an asynchronous HTTP (Ajax) request.
    // 비동기 통신 Ajax를 Setting한다.
    $.ajaxSetup({
        type:"POST",
        async:true,
        dataType:"json",
        error:function(xhr) {
            console.log("error html = " + xhr.statusText);
        }
    });
    
    $(function() {
        $("#commentWrite").on("click"function() {
            $.ajax({
                url:"/bbs/commentWrite.bbs",
                // data:{}에서는 EL을 ""로 감싸야 한다. 이외에는 그냥 사용한다.
                data:{
                    commentContent:$("#commentContent").val(),
                    articleNumber:"${article.articleNumber}"
                },
                beforeSend:function() {
                    console.log("시작 전...");
                },
                complete:function() {
                    console.log("완료 후...");
                },
                success:function(data) {            // 서버에 대한 정상응답이 오면 실행, callback
                    if(data.result == 1) {            // 쿼리 정상 완료, executeUpdate 결과
                        console.log("comment가 정상적으로 입력되었습니다.");
                        $("#commentContent").val("");
                        showHtml(data.comments, 1);
                    }
                }
            })
        });
    });
 
    function showHtml(data, commPageNum) {
        let html = "<table class='table table-striped table-bordered' style='margin-top: 10px;'><tbody>";
        $.each(data, function(index, item) {
            html += "<tr align='center'>";
            html += "<td>" + (index+1+ "</td>";
            html += "<td>" + item.id + "</td>";
            html += "<td align='left'>" + item.commentContent + "</td>";
            let presentDay = item.commentDate.substring(010);
            html += "<td>" + presentDay + "</td>";
            html += "</tr>";
        });
        html += "</tbody></table>";
        commPageNum = parseInt(commPageNum);        // 정수로 변경
        // commentCount는 동기화되어 값을 받아오기 때문에, 댓글 insert에 즉각적으로 처리되지 못한다.
        if("${article.commentCount}" > commPageNum * 10) {
            nextPageNum = commPageNum + 1;
            html +="<input type='button' class='btn btn-default' onclick='getComment(nextPageNum, event)' value='다음 comment 보기'>";
        }
        
        $("#showComment").html(html);
        $("#commentContent").val("");
        $("#commentContent").focus();
    }
    
    function getComment(commPageNum, event) {
        $.ajax({
            url:"/bbs/commentRead.bbs",
            data:{
                commPageNum:commPageNum*10,
                articleNumber:"${article.articleNumber}"
            },
            beforeSend:function() {
                console.log("읽어오기 시작 전...");
            },
            complete:function() {
                console.log("읽어오기 완료 후...");
            },
            success:function(data) {
                console.log("comment를 정상적으로 조회하였습니다.");
                showHtml(data, commPageNum);
                
                let position = $("#showComment table tr:last").position();
                $('html, body').animate({scrollTop : position.top}, 400);        // 두 번째 param은 스크롤 이동하는 시간
            }
        })
    }
</script>
cs


  아래는 댓글을 쓰고 읽어들이는 시연 화면이다.


  아래는 해당 글에 대한 댓글을 commPageSize(댓글 개수)에 따라 읽어들이는 시연 화면이다.



[Ajax, 위키백과 참고1]