본문 바로가기
ToyProject

스프링 게시판 만들기 - 7 (1) (조회수, 댓글 작성)

by 완두완두콩 2022. 4. 4.

이전 스프링 게시판 만들기 - 6 에서 페이징 처리와 검색을 구현해보았다.

이번에는 간단하게 조회수를 올리는 기능과, 댓글 기능

을 구현해보려 한다.

 

https://dodokong.tistory.com/54

 

스프링 게시판 만들기 - 6 (게시판 구현 & 페이징 처리 & 검색)

지금까지의 포스팅에서는 Bootstrap 을 이용해서 화면을 만드는 법과, Spring Security 를 이용해서 로그인을 구현해보았습니다. 이번 포스팅에서는 게시판을 구현해보려 합니다. 게시판도 동일하게 Bo

dodokong.tistory.com

 

조회수 증가


Board 도메인은 다음과 같다. 처음 설계에서는 countVisit 이 없었는데 조회수 기능을 넣기 위해

Long 타입으로 추가하였다.

 

int, long 이 아닌 Long 타입을 쓰는 이유는

값이 설정되지 않는 경우, null 값을 넣기 위함이다.

int, long 의 경우 기본형인 (Primitive Type)이기 때문에 값이 없는 경우 0 이 설정돼 있다.

이는 값이 들어간 것인지 혼란을 야기케 할 수 있다고 생각해서 참조형 변수(Reference Type) 을 사용하였다.

그 후, 하단부에 update 를 하기 위한 updateVisit 을 생성하였다.

JPA 에서는 변경 감지와 병합 , 두가지로 update 를 할 수 있는데 병합의 경우

내가 입력하지 않은 값의 경우 값이 초기화되기 때문에 변경 감지를 사용해서 update 해야한다.

(update 쿼리 사용 X. JPA 에서도 권장하지 않는 스펙. 자세한 내용은 다음 링크 참조!)

https://dodokong.tistory.com/51

 

변경 감지(Dirty Checking) & 병합(Merge)

JPA 에는 따로 Update 에 관한 쿼리문이 존재하지 않고 ( @Query로 만드려면 만들겠지만 ) Update 를 변경 감지와 병합 두 방법을 통해서 실행하게 된다. 변경 감지(Dirty Checking) 과 병합(merge) 에 대해서

dodokong.tistory.com

 

다음은 BoardService 에 update 를 구현하는 것이다.

Controller 에서 id 를 넘겨주고, 해당 Id 가 없는 경우 Exception 을 통해 에러를 터뜨리고,

존재하는 ID 인 경우 방금 위에서 만들었던 updateVisit 을 호출해서 조회수를 업데이트 해준다.

@PathVariable 을 통해 id 값에 따라 페이지 URL 이 설정되도록 하였고, 만약 1번 게시글을 클릭하는 경우

다음과 같이 URL 이 이동됨을 확인할 수 있다.

@GetMapping 이므로 해당 게시글을 조회할 때마다 아래의 코드가 호출되는데,

하단의 model.addAttribute 의 경우 프론트 단에서 해당하는 데이터를 보여주기 위해서 작성한 코드이다.

 

게시글의 경우, 현재 내가 클릭한 게시글의 번호(id)를 이용해서 해당하는 board 를 찾고,

그 board 의 countVisit(조회수)의 값을 가져온다.

방문할 때마다 조회수를 +1 씩 증가시키고, 증가시킨 값을 dto 에 담아서 updateVisit 에 반환한다.

그렇게 되면 boardService 에서 검증한 뒤, 값을 update 시켜준다.

따로 update 쿼리를 작성하지 않고 영속 엔티티로 존재하는 값을 변경시켜서(더티체킹을 통해)

조회수를 늘려주었다는 부분이 중요한 부분이라고 생각된다. 

조회수가 증가!

 

댓글 


간단하게 댓글 기능을 구현하기 위해 새로운 테이블을 추가하였다.

BoardComment 테이블을 생성하였고, 내용, 작성일, 작성자, 추후 댓글 삭제 기능을 위해

delete_check 을 넣어서 삭제 버튼을 누를 시, delete_check 를 'Y' 로 바꾸고 삭제를 시키는 

기능 등을 구현해 보려 한다.

 우선은 간단하게 댓글만 구현해보도록 하겠다.

 

boardContent.html

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
94
95
96
97
98
99
100
101
102
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
    <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.22/css/jquery.dataTables.css">
    <script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.22/js/jquery.dataTables.js"></script>
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .fakeimg {
            height: 200px;
            background: #aaa;
        }
    </style>
    <link href="static/css/headers.css" rel="stylesheet">
</head>
<body>
 
<!--<div class="jumbotron text-center" style="margin-bottom:0">-->
<!--    <h1>게시판</h1>-->
<!--</div>-->
 
<header class="p-3 bg-dark text-white">
    <div class="container">
        <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
            <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
                <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap"><use xlink:href="#bootstrap"/></svg>
            </a>
 
            <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                <li><a href="#" class="nav-link px-2 text-secondary">Home</a></li>
                <li><a href="#" class="nav-link px-2 text-white">Board</a></li>
                <li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
                <li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
                <li><a href="#" class="nav-link px-2 text-white">About</a></li>
            </ul>
 
            <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3">
                <input type="search" class="form-control form-control-dark" placeholder="Search..." aria-label="Search">
            </form>
 
            <div class="text-end">
                <button type="button" class="btn btn-outline-light me-2" onclick="moveLogin();">Login</button>
                <button type="button" class="btn btn-warning" onclick="moveJoin();">Sign-up</button>
            </div>
        </div>
    </div>
</header>
 
<div class="container" style="margin-top:30px">
    <div class="row">
        <div class="col-sm-12">
                <div class="form-group">
                    <h5 th:text="'제목 : ' + ${board.title}"></h5>
                </div>
                <div>
                    <td th:text="'작성자 : ' + ${board.createdBy}"></td>
                    <br><br>
                </div>
            <h5> 내용 </h5>
                <div style="border:1px solid; padding:10px;">
                    <dl>
                        <dd th:text="${board.content}"></dd>
                    </dl>
                </div>
        </div>
    </div>
</div>
<form action="boardContent.html" th:action method="post">
<div class="comment-form" style="text-align: center;">
    <div class="comment-form2" style="width:300px;height: 200px;display: inline-block">
    <label for="content">댓글 달기</label>
    <textarea class="form-control" id="content" name="content" rows="3"></textarea>
        <button type="submit" class="btn btn-primary" onclick="window.location.reload()">작성</button>
    </div>
</div>
</form>
<div class="container">
    <table class="table table-hover">
    <tr>
        <th>번호</th>
        <th>작성자</th>
        <th>내용</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="comment : ${comments}">
        <td th:text="${comment.id}"></td>
        <td th:text="${comment.created_by}"></td>ss
        <td th:text="${comment.content}"></td>
 
    </tr>
    </tbody>
    </table>
</div>
</body>
</html>
cs

74 - 99 번째 줄까지가 댓글을 구현하기 위한 기본 UI 와 출력하기 위해

Thymeleaf 로 작성한 코드이다. 이전의 코드들과 동일하게 comments 를 model 에 담아서

List 형태로 넘겨주면 th:each 를 통해 하나씩 출력하는 방식이다.

 

BoardComment

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
package toyproject.board.domain;
 
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
 
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
 
@Entity
@Table(name = "board_comment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BoardComment {
 
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_comment_id")
    private Long id;
    private String content;
    private LocalDateTime created_date;
    private String created_by;
    private Character delete_check;
 
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Board board;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    private Member member;
 
    @Builder
    public BoardComment(String content, LocalDateTime created_date, String created_by, Character delete_check, Board board, Member member) {
        this.content = content;
        this.created_date = created_date;
        this.created_by = created_by;
        this.delete_check = delete_check;
        if(this.board != null){
          board.getBoardCommentList().remove(this);
        }else
            this.board = board;
        if(this.member != null){
            member.getBoardCommentList().remove(this);
        }else
            this.member = member;
    }
}
 
cs

위 테이블에서 설계한 대로 작성하고, 연관관계 매핑을 해주었다.

Dto 에는 dto 를 BoardComment 타입으로 받기 위해 toEntity 만 추가 선언하였다.

Repository 를 추가하여 내가 작성한 board.id 에 해당하는 댓글만 출력할 수 있는 쿼리문을

작성하였다. ( 3번 게시글을 클릭하면 3번에 대한 댓글만 출력)

Controller

게시글을 작성하는 부분과 달라지는 부분들이 크게 없다. 계속해서 응용만 할뿐!..

똑같이 로그인한 유저정보를 받아와서 해당 유저정보를 작성자에 넣고,

엔티티 값을 넣은 뒤, model 에 담아서 반환한다.

 

테스트를 진행하던 중, 계속 1번 댓글의 값이 출력이 안되고 2번 부터 출력됐던 문제가 있었는데

단순히 List<BoardComment> comment = boardCommentRepository.findCommentBoardId(id); 부분을

마지막이 아닌 코드의 초반부에 적어서 문제가 생겼었다.

데이터가 전부 담긴 뒤, 보내줘야 하는데 데이터를 먼저 보내주고 save 를 하니 이런 문제가!..

쓸까 말까 하던 단순한 오류였지만 나중에 똑같은 실수를 하지 않기 위해서!..

 

결과

 


사담

 

열심히 해보려 하긴 하는데.. 음.. Sketcher 프로젝트에서 처럼

뭔가 음.. 엉성한 코드임이 느껴진다. 대충 굴러가기만 하는 코드인 느낌!

고수 개발자분들의 아~ 이런 부분은 이렇게 작성하셔야 좋은 코드에요~

하고 아는 방법을 알고 싶다..

 

당분간은 Sketcher 프로젝트에서 테스트 코드 작성과 코드 리팩토링 + 중간고사 준비로 인해

개인 프로젝트는 잠시 뒤로 미뤄놔야 할 것 같다!..

한창 재밌게 하는 중이였는데 조금 아쉽다!

 

다음에 돌아오면 인프라 세팅을 하고 배포를 해보는 작업을 해볼까 한다! (아마도!..)

 

 

다음 편!

 

스프링 게시판 만들기 - 7 (2) (동시성 feat.ThreadLocal)

한동안 손을 떼고 있던 토이프로젝트(게시판 만들기)를 간간히 봐주시는 분들이 계시는데 조회수 관련해서 동시성은 고려하신거냐고 묻는 댓글을 보고 음.. 동시성?.. 바로 이전 학기에 네트워

dodokong.tistory.com

 

댓글