대댓글 게시판 구현하기 (수정, 삭제 기능 x, 회원 존재 x)
프로젝트 폴더 및 파일 구성
대댓글을 구현하기 앞서 기본적인 게시판을 구현해야 한다.
기본 게시판 구현
- 게시물 목록
- 게시물 상세 보기
- 게시물 작성하기
게시물이 작성되어 저장하기 위한 table 구성 (mysql 기준)
* ip를 넣어놓은 이유는 익명일 때는 구분을 위해 IPv4 뒤 두 자리를 넣어놓던데… 사실 필요 없을 듯하다
- seq 값은 게시물의 pk 값
- 게시물은 제목(title)과 내용(content)을 가지고 있고,
그 게시물을 쓴 주체(user)에 대해 표시되어야 하기에 title, content, user가 존재 - 작성 시간(createAt)도 표시
mysql create sql
CREATE TABLE `post` (
`seq` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(50) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`content` varchar(100) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`user` varchar(20) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`ip` varchar(50) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`createAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci
화면은 총 3개로 구성
- 게시물 목록 화면 - /postBoard
- 목록 화면에서 게시물 제목 클릭 시 상세화면으로 넘어가기
- 게시물 작성 화면 - /postBoard/writePost
- 작성 화면은 Get으로 매핑
- 작성 후 등록 시 Post로 매핑, 등록 완료 후 목록 화면으로 돌아가기
- 게시물 상세 화면 - /postBoard/postDetail? seq={post seq 값}
Entity, Repository, Service, Controller를 구성해 보자.
우선 Entity
테이블의 row값을 매핑할 수 있게 위에 구성한 table을 참고해 만들어준다.
@Table(name="post")
@Entity
@DynamicInsert
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long seq;
private String title;
private String content;
private String user;
private String ip;
@Column(name = "createAt")
private LocalDateTime createAt;
public JSONObject getPostJson() {
JSONObject result = new JSONObject();
result.put("seq", this.getSeq());
result.put("title", this.getTitle());
result.put("content", this.getContent());
result.put("user", this.getUser());
result.put("createAt", this.getCreateAt());
return result;
}
}
중간에 getPostJson 메서드는 entity를 Json으로 바꾸기 위한 메서드다.
Repository 선언
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}
Service 선언
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
// 차후 post 엔티티와 연관된 댓글을 가져오기 위한 post 엔티티 그자체 조회
public Post getPostEntity(Long seq) {
Optional<Post> optionalPost = postRepository.findById(seq);
return optionalPost.orElse(null);
}
// 게시물 목록을 보여주기 위해 모든 게시물 조회 (실무에선 이럼 안되용~)
public JSONArray getPostAll() {
JSONArray result = new JSONArray();
postRepository.findAll().forEach(post -> {
JSONObject postJson = new JSONObject();
postJson.put("seq", post.getSeq());
postJson.put("title", post.getTitle());
postJson.put("content", post.getContent());
postJson.put("createAt", post.getCreateAt());
postJson.put("user", post.getUser());
result.put(postJson);
});
return result;
}
// 게시물 등록을 위한 메소드
@Transactional
public void savePost(Post post) {
postRepository.save(post);
}
}
위에 주석을 적어놨지만 하나씩 떼서 설명을 해보자면
게시물 목록을 보여주기 위해서는 목록을 만들기 위해
( 제목, 상세페이지로 이동하기 위한 seq, 작성시각, 작성자 ) 등의 정보가 필요하다.
*사실 목록을 만들 때 내용(content) 정보는 필요 없다.
public JSONArray getPostAll() {
JSONArray result = new JSONArray();
postRepository.findAll().forEach(post -> {
JSONObject postJson = new JSONObject();
postJson.put("seq", post.getSeq());
postJson.put("title", post.getTitle());
postJson.put("content", post.getContent());
postJson.put("createAt", post.getCreateAt());
postJson.put("user", post.getUser());
result.put(postJson);
});
return result;
}
게시물 등록 시 사용하는 메서드
@Transactional
public void savePost(Post post) {
postRepository.save(post);
}
게시물과 연관된 댓글을 가져오기 위한 게시물 자체의 entity가 필요해서
이 부분은 게시물 댓글 조회 시 다시 살펴보자.
public Post getPostEntity(Long seq) {
Optional<Post> optionalPost = postRepository.findById(seq);
return optionalPost.orElse(null);
}
Controller
- 글목록
@Controller
public class boardController{
@Autowired
private PostService postService;
@GetMapping("/")
public String getBoardPage(Model model) {
model.addAttribute("posts",postService.getPostAll());
return "board";
}
}
위에 적은 메서드 대로 “/” 접근 시 모든 게시물 정보를 조회해 목록 페이지로 반환한다.
참고로 페이지는 jsp로 구성했다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<title>게시판</title>
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
</head>
<body>
<ul>
<li><a href="/postBoard">글목록</a></li>
<li><a href="writePost">글작성</a></li>
</ul>
<h3>게시판 글목록</h3>
<table id="post_table">
<thead>
<tr>
<th>제목</th>
<th>작성자</th>
<th>작성시간</th>
<tr>
</thead>
<tbody>
<tr>
<tr>
</tbody>
</table>
</body>
<script type="text/javascript">
var posts = ${posts};
</script>
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/board.js"></script>
</html>
실제로 구성된 화면은 아래와 같다.
글목록과 글작성 메뉴가 존재하고 글목록 클릭 시 현재 페이지로 이동된다.
글작성
@Controller
public class boardController{
@Autowired
private PostService postService;
// ...
@GetMapping("/writePost")
public String getWriteBoardPage() {
return "write";
}
}
write page
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<title>게시판</title>
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
</head>
<body>
<ul>
<li><a href="/postBoard">글목록</a></li>
<li><a href="writePost">글작성</a></li>
</ul>
<h3>게시판 글쓰기</h3>
<section>
<dt>제목</dt>
<dd>
<input
type="text"
id="title"
placeholder="제목 필수"
/>
</dd>
<dt>내용</dt>
<dd>
<textarea
id="content"
cols="50" rows="10"
placeholder="내용"
></textarea>
</dd>
<dt>작성자</dt>
<dd>
<input
type="text"
id="user"
placeholder="작성자명"
/>
</dd>
<dd>
<input type="button" id="submit" value="등록"/>
</dd>
</section>
</body>
<script
type="text/javascript"
src="${pageContext.request.contextPath}/resources/js/write.js"></script>
</html>
작성된 내용은 등록 버튼 클릭시 처리되어 post로 넘기게 된다.
구현된 화면은 아래와 같다.
작성페이지에서 post로 넘기는 함수 구현 (write.js)
$("#submit").off("click").on("click", function(){
let data = {
"title" : $("#title").val(),
"content" : $("#content").val(),
"user" : $("#user").val(),
};
$.ajax({
method : "POST",
url : "writePost",
data : JSON.stringify(data),
contentType: 'application/json',
success : function() {
location.href = "/postBoard";
}
});
});
write post mapping
@Contorller
public class boardContorller {
@Autowired
private PostService postService;
...
@PostMapping("/writePost")
@ResponseStatus
public ResponseEntity<Post> insertPost(@RequestBody InsertPostDto postDto,
HttpServletRequest request) {
String ip = request.getHeader("X-FORWARDED-FOR");
if (ip == null)
ip = request.getRemoteAddr();
postDto.setIp(ip);
Post post = postDto.toEntity();
postService.savePost(post);
return ResponseEntity.status(200).build();
}
}
메서드의 parameter로 DTO 값을 받아 entity로 변환하고 있다. DTO를 살펴보자
@Data
public class InsertPostDto {
private String title;
private String content;
private String user;
private String ip;
public Post toEntity(){
return Post.builder()
.title(this.title)
.content(this.content)
.user(this.user)
.ip(this.ip)
.build();
}
}
화면상에서 구성한 입력값을 서버로 보내기 위한 DTO로 입력값을 entity로 변환하는 toEntity 메서드를 포함하고 있다.
게시물 상세 화면
@Contorller
public class boardContorller {
@Autowired
private PostService postService;
...
@GetMapping("/postDetail")
public String getPostDetail(@RequestParam(name = "seq") Long seq, Model model) {
Post post = postService.getPostEntity(seq);
model.addAttribute("postDetail", post.getPostJson());
return "detail";
}
}
게시물 목록에서 게시물 클릭 시 /postDetail로 연결되어 넘겨받은 게시물의 id (seq)로 해당 게시물 정보를 가져온 뒤, detail 페이지를 반환한다.
상세 보기에는 댓글 등록 및 조회가 되어야 하므로 댓글 기능 추가를 설명하면서 같이 살펴보기로 하자.
댓글 기능 추가
기능 추가 전 어떤 기능이나 특징이 필요한지 나열해 보면
- 댓글은 게시물에 종속되어 있다.
- 대댓글을 무한히 달 수 있고, 대댓글은 댓글보다 안으로(?) 들여 쓰기 되어 있다.
나도 안 만들어 봤기에 다른 블로그 글을 참고했다.
해당 블로그 주인분 께서는 REF, REF_STEP, REF_LEVEL이라는 필드 값을 두고 대댓글을 구현하셨는데
각 필드값의 의미를 보자면
REF - 댓글의 그룹
- 첫 번째 댓글의 밑으로 달리는 대댓글은 첫번째 댓글 “그룹”으로 묶이며 두 번째 댓글과 구별해 주는 역할을 한다.
REF_STEP - 댓글의 순서
- 그룹 내 댓글의 순서를 지정해 주는 역할이다.
- 가령, 첫 번째 댓글(1)의 대댓글(1-1), (1-2)가 존재할 때, 대댓글의 대댓글(1-1-1)을 달게 되면 기존의 대댓글(1-2)은 (1-1-1) 보다 뒤로 밀려나야 한다. 이때 STEP 필드 값을 +1 해줘 순서를 보장해 준다.
REF_LEVEL - 댓글의 깊이
- 댓글과 대댓글 간에 깊이 차이를 둬 어떤 댓글의 대댓글인지 확인할 수 있게 하는 역할
이 글을 보고 구현하던 중 무언가 잘못됐다 느낀 지점이 있었는데 바로 REF_STEP이다.
만약 대댓글이 (1-1), (1-2), (1-3), (1-4)로 구성되어 있다고 하자. (1번 댓글의 대댓글이다)
이때 (1-1)에 대댓글을 달게 되면 (1-1-1)은 STEP은 3으로 삽입되게 된다. (1, 1-1, 1-1-1의 순서로)
문제는 기존의 대댓글들은 DB에 STEP이 박혀있을 텐데, 새로운 STEP이 중간에 끼면서 그다음 순서댓글들
역시 STEP +1씩 해주어야 한다는 사실이다.
댓글은 하나 적는데 업데이트는 여러 개 되는 상황이 생기면서 ‘이게 맞나?’ 싶었다.
그래서 좀 더 고민해 보고 아래와 같이 구성했다.
하나씩 살펴보자면
- seq → 댓글의 id(pk) 값이다.
- postSeq → 댓글은 게시물에 종속되기에 1:N pk-fk 관계를 가진다.
- REF_ → 댓글의 그룹을 표시한다. 그룹의 숫자가 작을수록 먼저 달린 댓글로 표시되고 대댓글의 그룹을 구분 짓는다.
- REF_LEVEL → 대댓글의 깊이를 나타낸다. 0은 게시물에 달린 댓글, level > 0 일 때는 댓글 혹은 대댓글의 댓글인 셈이다.
- user, ip, content → 작성자 정보와 작성 내용이다.
- createAt → 작성시각
- parent → 대댓글이 어떤 댓글에 답글을 단 건지 적는 필드이다. 게시물에 바로 다는 댓글은 부모댓글이 없기 때문에 -1로 적어 넣는다.
위 구현과 비교하면 STEP 값이 없다.
구현 로직은 다음과 같다.
게시물에 바로 댓글을 달면 REF_ = 0, REF_LEVEL = 0, parent = -1로 저장한다.
해당 댓글에 대댓글을 작성 시
- REF_ = 0 : 댓글의 그룹은 최상위 댓글과 동일하다.
- REF_LEVEL = 1 : 대댓글의 경우, 부모 댓글의 LEVEL에 +1 해준 값을 가진다.
- parent = 부모 댓글의 seq : 대댓글의 경우, 부모 댓글에 seq 값을 가진다.
table을 구성해 보자. (구현하면서 중간에 바꾼 거라 REF_ORDER 필드가 남아있다.)
CREATE TABLE `comment` (
`seq` bigint NOT NULL AUTO_INCREMENT,
`postSeq` bigint DEFAULT NULL,
`REF_` int DEFAULT NULL COMMENT '댓글 그룹',
`REF_ORDER` int DEFAULT NULL COMMENT '대댓글 작성시 순서',
`REF_LEVEL` int DEFAULT NULL COMMENT '대댓글 깊이',
`user` varchar(20) DEFAULT NULL,
`ip` varchar(100) DEFAULT NULL,
`content` varchar(100) DEFAULT NULL,
`createAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`parent` bigint DEFAULT '-1',
PRIMARY KEY (`seq`),
KEY `postSeq` (`postSeq`),
CONSTRAINT `comment_ibfk_1` FOREIGN KEY (`postSeq`) REFERENCES `post` (`seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
차례대로
- Entity, Repository, Service를 구성하고
- 게시물 상세 보기 시 댓글을 같이 반환하는 메서드 추가
- html 화면 내 댓글 목록과 댓글 작성 부분까지
보기로 하자.
comment entity
@Table(name="comment")
@Entity
@DynamicInsert
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long seq;
@ManyToOne(targetEntity = Post.class, cascade = CascadeType.ALL)
@JoinColumn(name="postSeq")
private Post post;
@Column(name="REF_")
private int REF;
@Column(name="REF_ORDER")
private int REFORDER;
@Column(name="REF_LEVEL")
private int REFLEVEL;
private String user;
private String ip;
private String content;
@Column(name="createAt")
private LocalDateTime createAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="parent", referencedColumnName = "seq")
private Comment parent;
public String parentName(){ return this.parent != null ? this.parent.getUser() : ""; }
public Long parentSeq() { return this.parent != null ? this.parent.getSeq() : -1L; }
}
entity에서 특징적인 부분만 살펴보자
게시물과 종속 관계에 있으니 post entity와 연관 관계를 표시해준다. 다대일 관계니 manyToOne
@ManyToOne(targetEntity = Post.class, cascade = CascadeType.ALL)
@JoinColumn(name="postSeq")
private Post post;
대댓글과 댓글의 관계 역시 다대일 연관 관계이므로
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="parent", referencedColumnName = "seq")
private Comment parent;
entity를 화면상으로 반환하기 전 편의를 위해 해당 댓글의 부모댓글의 seq, user 이름등을 포함하기 위한 메서드를 구성
public String parentName(){ return this.parent != null ? this.parent.getUser() : ""; }
public Long parentSeq() { return this.parent != null ? this.parent.getSeq() : -1L; }
comment Repository
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
public List<Comment> findByPostOrderByREFAsc(Post post);
}
Post repository와 다르게 하나 선언된 메서드가 있는데 게시물에 딸린 댓글을 가져올 때 REF(댓글 그룹) 오름차순으로 가져오는 메서드다. 먼저 달린 댓글일수록 REF 값은 작게끔 구현이 되기 때문에 조회 시 해당 메서드를 사용한다.
comment service
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Transactional
public void saveComment(InsertCommentDto insertCommentDto) {
commentRepository.save(insertCommentDto.toEntity());
}
public JSONArray getComments(Post post) {
JSONArray comments = new JSONArray();
commentRepository.findByPostOrderByREFAsc(post).forEach(comment -> {
JSONObject jsonObject = new JSONObject();
jsonObject.put("seq", comment.getSeq());
jsonObject.put("REF", comment.getREF());
jsonObject.put("REFORDER", comment.getREFORDER());
jsonObject.put("REFLEVEL", comment.getREFLEVEL());
jsonObject.put("user", comment.getUser());
jsonObject.put("content", comment.getContent());
jsonObject.put("createAt", comment.getCreateAt());
jsonObject.put("parentName", comment.parentName());
jsonObject.put("parentSeq", comment.parentSeq());
comments.put(jsonObject);
});
return comments;
}
public Comment getCommentBySeq(Long seq) {
return commentRepository.findById(seq).orElse(null);
}
}
save… 메서드는 이름 그대로 댓글을 저장하는 함수,
get… 메서드들은 이름 그대로 조건에 맞춰 댓글(들)을 가져오는 함수다.
save의 경우엔 InsertCommentDto를 통해 입력받고 있다.
@PostMapping("/commentInsert")
@ResponseStatus
public ResponseEntity<Comment> uploadComment(@RequestBody InsertCommentDto commentDto,
HttpServletRequest request){
String ip = request.getHeader("X-FORWARDED-FOR");
if (ip == null)
ip = request.getRemoteAddr();
commentDto.setIp(ip);
System.out.println(commentDto);
Post postEntity = postService.getPostEntity(commentDto.getPostSeq());
commentDto.setPost(postEntity);
// parent 존재시
if(commentDto.getParentSeq() != null){
Comment comment = commentService.getCommentBySeq(commentDto.getParentSeq());
commentDto.setParent(comment);
}
commentService.saveComment(commentDto);
return ResponseEntity.status(200).build();
}
입력받을 때 ip값은 빠져있기에 ip 값을 추가해 주고,
parentSeq 값이 존재한다면, 부모댓글을 조회해 해당 entity 정보를 세팅한 뒤 저장해 준다.
insertCommentDto는 아래와 같다.
@Data
public class InsertCommentDto {
private String user;
private String content;
private String ip;
private int ref;
private int refOrder;
private int refLevel;
private Long postSeq;
private Post post;
private Long parentSeq;
private Comment parent;
public Comment toEntity() {
return Comment.builder()
.user(this.user)
.content(this.content)
.ip(this.ip)
.REF(this.ref)
.REFORDER(this.refOrder)
.REFLEVEL(this.refLevel)
.post(this.post)
.parent(this.parent)
.build();
}
}
위에서 설명하다 만 부분이 있다.
바로 게시물 상세 보기에서 댓글을 포함해 반환해야 한다고 말했는데
@GetMapping("/postDetail")
public String getPostDetail(@RequestParam(name = "seq") Long seq, Model model) {
Post post = postService.getPostEntity(seq);
JSONArray comments = commentService.getComments(post); // 추가된 부분
model.addAttribute("postDetail", post.getPostJson());
model.addAttribute("comments", comments); // 추가된 부분
return "detail";
}
게시물 entity를 이용해 연관관계인 comment를 조회한다. 조회된 Comment 정보를 댓글로 반환한다.
구현 다하고 보니 골치 아픈 부분이 있었는데 가령 아래와 같은 데이터가 있을 때
[
{"REFLEVEL":0,"REF":0,"parentName":"","parentSeq":-1,"REFORDER":0,"user":"익명4c587c","seq":34,"content":"첫번째 댓글","createAt":"2023-08-02T10:32:28"},
{"REFLEVEL":1,"REF":0,"parentName":"익명4c587c","parentSeq":34,"REFORDER":0,"user":"익명5b982c","seq":35,"content":"대댓글?","createAt":"2023-08-02T10:32:35"},
{"REFLEVEL":0,"REF":2,"parentName":"","parentSeq":-1,"REFORDER":0,"user":"익명9a1444","seq":36,"content":"두번째 댓글","createAt":"2023-08-02T10:33:12"},
{"REFLEVEL":2,"REF":0,"parentName":"익명5b982c","parentSeq":35,"REFORDER":0,"user":"익명5b37af","seq":37,"content":"대대댓글","createAt":"2023-08-02T10:33:19"},
{"REFLEVEL":2,"REF":0,"parentName":"익명5b982c","parentSeq":35,"REFORDER":0,"user":"익명f654ad","seq":38,"content":"대대댓글2","createAt":"2023-08-02T10:34:10"},
{"REFLEVEL":3,"REF":0,"parentName":"익명5b37af","parentSeq":37,"REFORDER":0,"user":"익명d3c426","seq":39,"content":"대대대댓글","createAt":"2023-08-02T10:34:37"},
]
댓글의 순서는 아래와 같아야 한다. (가시성을 위해 몇몇 필드 제거)
{"REFLEVEL":0,"REF":0,"parentSeq":-1,"seq":34,"createAt":"2023-08-02T10:32:28"}
{"REFLEVEL":1,"REF":0,"parentSeq":34,"seq":35,"createAt":"2023-08-02T10:32:35"}
{"REFLEVEL":2,"REF":0,"parentSeq":35,"seq":37,"createAt":"2023-08-02T10:33:19"}
{"REFLEVEL":3,"REF":0,"parentSeq":37,"seq":39,"createAt":"2023-08-02T10:34:37"}
{"REFLEVEL":2,"REF":0,"parentSeq":35"seq":38,"createAt":"2023-08-02T10:34:10"}
{"REFLEVEL":0,"REF":2,"parentSeq":-1,"seq":36,"createAt":"2023-08-02T10:33:12"}
만들면서 헷갈린 부분이 바로 여기 있다.
REF 순으로 정렬까지는 ok, 이전에 STEP 필드값은 update가 너무 많이 일어나기 때문에 사용하지 않는다고 했다.
그럼 첫 번째 댓글 그룹 중에서 REF_STEP 값 없이 어떻게 정렬해야 할까?
시간 순으로 한다고 하면 이전 댓글(1-1)의 대댓글(1-1-1)이 (1-2)보다 나중에 달렸을 때 정렬 시
(1-1) → (1-2) → (1-1-1)로 정렬되어 대댓글이 잘못 달린 것처럼 보일 것이다.
seq 값 역시 시간 순과 마찬가지로 증가만 하기 때문에 정렬에 사용할 수 없고,
parentSeq (부모댓글의 seq)로 정렬하면 시간 순 정렬과 마찬가지로 부모댓글의 seq가 작은 순으로 정렬된다.
그래서 내가 쓴 트릭은 이렇다.
한 가지 가정이 들어간다.
(게시물에 댓글은 많이 달리지 않을 거다… 한 200개 정도로?)
dfs를 사용하는 거다.
우선 parentSeq가 -1인 댓글을 순회하며 찾는다. (게시물에 달린 댓글 = c)
해당 댓글들에 parentSeq가 위 댓글들의 seq인 댓글을 순회하면서 찾는다. (c에 대댓글 = c’)
대댓글의 대댓글을 재귀의 형태로 찾아 내려간다.
위 로직을 구현하면 아래와 같다.
댓글의 순서는 (REF_, parentSeq, seq) 값으로 결정
[(0, -1, 1), (0, 1, 2), (0, 1, 3), (0, 2, 5), (1, -1, 4)] 일때
1. parentSeq = -1 인 댓글을 찾는다. 게시물에 직접 달린 댓글이므로
[ (0, -1, 1), (1, -1, 4) ]
2. 해당 댓글들의 seq를 parentSeq로 가진 댓글을 찾는다.
먼저 (0, -1, 1) 의 대댓글은
[ (0, 1, 2), (0, 1, 3) ] 이렇게 두개다.
3. 2번에서 찾은 댓글의 대댓글을 찾는다.
(0, 1, 2) 의 대댓글은
[ (0, 2, 5) ] 이다. 해당 댓글의 대댓글은 없으므로 3번 과정이 종료.
해당 댓글을 반환한다.
4. 3번 과정에서 반환한 값 [ (0, 2, 5) ]을 부모댓글 뒤에 넣어준다.
[ (0, 1, 2), (0, 2, 5), (0, 1, 3) ]
5. 2번 과정에서 반환한 값 [ (0, 1, 2), (0, 2, 5), (0, 1, 3) ]을 부모댓글 뒤에 넣어준다.
[ (0, -1, 1), (0, 1, 2), (0, 2, 5), (0, 1, 3), (1, -1, 4) ]
정렬된 순서대로 화면에 보여준다.
(0, -1, 1)
(0, 1, 2)
(0, 2, 5)
(0, 1, 3)
(1, -1, 4)
서버에서는 게시물에 딸린 댓글을 댓글 그룹순으로 전송해 준다. 정렬 없이!
전송된 댓글과 대댓글은 위 로직(dfs) 의 형태로 재정렬 된다.
let comments =
[
{"REFLEVEL":0,"REF":0,"parentName":"","parentSeq":-1,"REFORDER":0,"user":"익명4c587c","seq":34,"content":"첫번째 댓글","createAt":"2023-08-02T10:32:28"},
{"REFLEVEL":1,"REF":0,"parentName":"익명4c587c","parentSeq":34,"REFORDER":0,"user":"익명5b982c","seq":35,"content":"대댓글?","createAt":"2023-08-02T10:32:35"},
{"REFLEVEL":0,"REF":2,"parentName":"","parentSeq":-1,"REFORDER":0,"user":"익명9a1444","seq":36,"content":"두번째 댓글","createAt":"2023-08-02T10:33:12"},
{"REFLEVEL":2,"REF":0,"parentName":"익명5b982c","parentSeq":35,"REFORDER":0,"user":"익명5b37af","seq":37,"content":"대대댓글","createAt":"2023-08-02T10:33:19"},
{"REFLEVEL":2,"REF":0,"parentName":"익명5b982c","parentSeq":35,"REFORDER":0,"user":"익명f654ad","seq":38,"content":"대대댓글2","createAt":"2023-08-02T10:34:10"},
{"REFLEVEL":3,"REF":0,"parentName":"익명5b37af","parentSeq":37,"REFORDER":0,"user":"익명d3c426","seq":39,"content":"대대대댓글","createAt":"2023-08-02T10:34:37"},
]
let parents = [];
comments.forEach((comment) => {
if(comment.parentSeq == -1) parents.push(comment);
});
function dfs(orgComment, comments) {
let result = [orgComment];
for(let i = 0; i < comments.length; i++) {
let comment = comments[i];
if(orgComment.seq == comment.parentSeq) {
// result.push(comment);
let children = dfs(comment, comments.slice(i, comments.length));
result = result.concat(children);
}
}
return result;
}
let result = [];
for(let comment of parents) {
let children = dfs(comment, comments);
result = result.concat(children);
}
화면을 그리는 html 은 아래와 같다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%> <%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<title>게시판</title>
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twbs-pagination/1.4.2/jquery.twbsPagination.min.js"></script>
</head>
<body>
<header>
<ul>
<li><a href="/postBoard">글목록</a></li>
<li><a href="writePost">글작성</a></li>
</ul>
</header>
<div class="cont">
<h3>게시글</h3>
<section width="500px" height="500px">
<dt>제목</dt>
<dd>
<div id="title">
</div>
</dd>
<dt>내용</dt>
<dd>
<div id="content">
</div>
</dd>
<dt>작성자</dt>
<dd>
<div id="user">
</div>
</dd>
<dt>작성시간</dt>
<dd>
<div id="createAt">
</div>
</dd>
</section>
<section>
<h6>댓글 등록</h6>
<dt>작성자</dt>
<dd><input id="commentUser"/></dd>
<dt>댓글</dt>
<dd><input id="commentContent"/></dd>
<dd><input type="button" id="commentSubmit" value="댓글등록"/></dd>
</section>
<section>
<h6>댓글 목록</h6>
<div id="commentList">
</div>
<div id="pagination-div" ></div>
</section>
</div>
</body>
<script type="text/javascript">
var postDetail = ${postDetail};
var comments = ${comments};
</script>
<script
type="text/javascript"
src="${pageContext.request.contextPath}/resources/js/detail.js"></script>
</html>
구현화면은 아래와 같다.
게시물의 댓글을 다는 부분과 댓글 별로 대댓 작성 버튼이 달려있고, 대댓 작성 버튼을 누를 경우,
대댓글 작성 부분이 등장해 대댓글을 작성할 수 있게 해 준다.
댓글의 깊이가 같은 레벨일 때는 시간 오름차순으로 정렬해 보여주게 된다.
모든 코드는 아래의 깃허브에
https://github.com/hoonzinope/SprintBootStudy/tree/main/demoBoard
'text > Java' 카테고리의 다른 글
검색 자동완성 with mysql function (5) | 2024.04.07 |
---|---|
jar 인자 전달을 쉽게! jCommander (0) | 2024.04.06 |
FK-FK table springboot @ID 매핑 (0) | 2023.06.21 |
spring boot login 중복 로그인 방지 (2) | 2023.05.28 |
spring boot 로그인 (+spring security, mysql) (1) | 2023.05.28 |
댓글