diff --git a/src/main/java/com/ll/spring_additional/base/initData/NotProd.java b/src/main/java/com/ll/spring_additional/base/initData/NotProd.java index 6ebd298..fd53966 100644 --- a/src/main/java/com/ll/spring_additional/base/initData/NotProd.java +++ b/src/main/java/com/ll/spring_additional/base/initData/NotProd.java @@ -13,6 +13,8 @@ import com.ll.spring_additional.boundedContext.answer.entity.Answer; import com.ll.spring_additional.boundedContext.answer.repository.AnswerRepository; import com.ll.spring_additional.boundedContext.answer.service.AnswerService; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; +import com.ll.spring_additional.boundedContext.comment.repository.CommentRepository; import com.ll.spring_additional.boundedContext.question.entity.Question; import com.ll.spring_additional.boundedContext.question.repository.QuestionRepository; import com.ll.spring_additional.boundedContext.question.service.QuestionService; @@ -29,7 +31,8 @@ CommandLineRunner initData( UserService userService, AnswerService answerService, QuestionRepository questionRepository, - AnswerRepository answerRepository + AnswerRepository answerRepository, + CommentRepository commentRepository ) { return new CommandLineRunner() { @@ -58,8 +61,8 @@ public void run(String... args) throws Exception { Question question1 = questionService.create("질문입니닷", "질문이에요!", user2, 0); Question question2 = questionService.create("질문입니닷22", "질문이에요!22", user2, 0); - Answer answer1 = answerService.create(question1, "답변1", user1); - Answer answer2 = answerService.create(question1, "답변2", user1); + Answer answer1 = answerService.create(question2, "답변1", user1); + Answer answer2 = answerService.create(question2, "답변2", user1); List answerList = new ArrayList<>(); for (int i = 1; i <= 300; i++) { @@ -71,6 +74,51 @@ public void run(String... args) throws Exception { } answerRepository.saveAll(answerList); + + List commentList = new ArrayList<>(); + for(int i=1; i <= 5; i++) { + Comment tmp = Comment.builder() + .content("테스트 댓글%d".formatted(i)) + .question(question2) + .writer(user2) + .build(); + commentList.add(tmp); + } + commentRepository.saveAll(commentList); + + Comment commentSecret1 = Comment.builder() + .content("테스트 비밀댓글") + .writer(user2) + .question(question2) + .secret(true) + .build(); + commentRepository.save(commentSecret1); + + Comment comment1 = Comment.builder() + .content("테스트 대댓글") + .writer(user2) + .question(question2) + .parent(commentRepository.findById(1L).get()) + .build(); + + commentRepository.save(comment1); + + Comment comment3 = Comment.builder() + .content("테스트 댓글") + .writer(user2) + .answer(answer1) + .build(); + + commentRepository.save(comment3); + + Comment comment4 = Comment.builder() + .content("테스트 대댓글") + .writer(user2) + .answer(answer1) + .parent(comment3) + .build(); + commentRepository.save(comment4); + } }; } diff --git a/src/main/java/com/ll/spring_additional/boundedContext/answer/entity/Answer.java b/src/main/java/com/ll/spring_additional/boundedContext/answer/entity/Answer.java index 1836e53..4f8946b 100644 --- a/src/main/java/com/ll/spring_additional/boundedContext/answer/entity/Answer.java +++ b/src/main/java/com/ll/spring_additional/boundedContext/answer/entity/Answer.java @@ -1,16 +1,20 @@ package com.ll.spring_additional.boundedContext.answer.entity; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; import com.ll.spring_additional.boundedContext.question.entity.Question; import com.ll.spring_additional.boundedContext.user.entity.SiteUser; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -20,9 +24,11 @@ import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.PrePersist; import lombok.Getter; import lombok.Setter; +import lombok.ToString; @Getter @Setter @@ -57,4 +63,8 @@ public void prePersist() { @ManyToMany private Set voters = new LinkedHashSet<>(); + + @OneToMany(mappedBy = "answer", cascade = {CascadeType.REMOVE}) + @ToString.Exclude + private List comments = new ArrayList<>(); } \ No newline at end of file diff --git a/src/main/java/com/ll/spring_additional/boundedContext/comment/controller/CommentController.java b/src/main/java/com/ll/spring_additional/boundedContext/comment/controller/CommentController.java new file mode 100644 index 0000000..094febc --- /dev/null +++ b/src/main/java/com/ll/spring_additional/boundedContext/comment/controller/CommentController.java @@ -0,0 +1,262 @@ +package com.ll.spring_additional.boundedContext.comment.controller; + +import java.security.Principal; + +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ResponseStatusException; + +import com.ll.spring_additional.boundedContext.answer.entity.Answer; +import com.ll.spring_additional.boundedContext.answer.service.AnswerService; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; +import com.ll.spring_additional.boundedContext.comment.form.CommentForm; +import com.ll.spring_additional.boundedContext.comment.service.CommentService; +import com.ll.spring_additional.boundedContext.question.entity.Question; +import com.ll.spring_additional.boundedContext.question.service.QuestionService; +import com.ll.spring_additional.boundedContext.user.entity.SiteUser; +import com.ll.spring_additional.boundedContext.user.service.UserService; + +import lombok.RequiredArgsConstructor; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/comment") +public class CommentController { + private final CommentService commentService; + private final QuestionService questionService; + private final AnswerService answerService; + private final UserService userService; + private final int PAGESIZE = 5; + + @GetMapping("/{type}/{id}") + public String showComments(Model model, @ModelAttribute CommentForm commentForm, + @RequestParam(defaultValue = "0") Integer commentPage, @PathVariable String type, @PathVariable Integer id, @RequestParam(required = false) Integer questionId) { + + Question question; + if (type.equals("question")) { + question= questionService.getQuestion(id); + model.addAttribute("question", question); + Page commentPaging = commentService.getCommentPageByQuestion(commentPage, question); + model.addAttribute("questionCommentPaging", commentPaging); + model.addAttribute("totalCount", commentPaging.getTotalElements()); + return "comment/question_comment"; + } + else { + question= questionService.getQuestion(questionId); + model.addAttribute("question", question); + Answer answer = answerService.getAnswer(id); + model.addAttribute("answer", answer); + Page commentPaging = commentService.getCommentPageByAnswer(commentPage, answer); + model.addAttribute("answerCommentPaging", commentPaging); + model.addAttribute("totalCount", commentPaging.getTotalElements()); + return "comment/answer_comment"; + } + } + + @PreAuthorize("isAuthenticated()") + @PostMapping("/create/{type}") + public String create(Model model, @ModelAttribute CommentForm commentForm, @PathVariable String type, Principal principal, + BindingResult bindingResult) { + + if (bindingResult.hasErrors()) { + return "question/question_detail"; + } + + SiteUser user = userService.getUser(principal.getName()); + String content = commentForm.getCommentContents().trim(); + Question question = questionService.getQuestion(commentForm.getQuestionId()); + + int lastPage; + + if (type.equals("question")) { + Comment comment = commentService.createByQuestion(content, commentForm.getSecret(), user, question); + lastPage = commentService.getLastPageNumberByQuestion(question); + Page commentPaging = commentService.getCommentPageByQuestion(lastPage, question); + model.addAttribute("questionCommentPaging", commentPaging); + model.addAttribute("question", question); + model.addAttribute("totalCount", commentPaging.getTotalElements()); + return "comment/question_comment :: #question-comment-list"; + } else { + Answer answer = answerService.getAnswer(commentForm.getAnswerId()); + Comment comment = commentService.createByAnswer(content, commentForm.getSecret(), user, answer); + lastPage = commentService.getLastPageNumberByAnswer(answer); + Page commentPaging = commentService.getCommentPageByAnswer(lastPage, answer); + model.addAttribute("answerCommentPaging", commentPaging); + model.addAttribute("question", question); + model.addAttribute("answer", answer); + model.addAttribute("totalCount", commentPaging.getTotalElements()); + return "comment/answer_comment :: #answer-comment-list"; + } + } + + @PreAuthorize("isAuthenticated()") + @PostMapping("/reply/create/{type}") + public String replyCreate(Model model, @ModelAttribute CommentForm commentForm, @PathVariable String type, BindingResult bindingResult, + Principal principal) { + + if (bindingResult.hasErrors()) { + return "question/question_detail"; + } + + Question question = questionService.getQuestion(commentForm.getQuestionId()); + SiteUser user = userService.getUser(principal.getName()); + + // 부모 댓글 찾아오기 + Comment parent = commentService.getComment(commentForm.getParentId()); + + if (parent == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "해당 댓글을 찾을 수 없습니다"); + } + + model.addAttribute("question", question); + + int page=0; + Page paging; + + // 자식 댓글 생성 + if (type.equals("question")) { + Comment comment = commentService.createReplyCommentByQuestion(commentForm.getCommentContents(), + commentForm.getSecret(), user, question, parent); + // 부모 댓글이 있는 페이지로 가야하므로, 부모의 페이지를 구해옴 + page = commentService.getPageNumberByQuestion(question, comment, PAGESIZE); + paging = commentService.getCommentPageByQuestion(page, question); + model.addAttribute("questionCommentPaging", paging); + // 전체 댓글 수 갱신 + model.addAttribute("totalCount", paging.getTotalElements()); + return "comment/question_comment :: #question-comment-list"; + } else { + Answer answer = answerService.getAnswer(commentForm.getAnswerId()); + model.addAttribute("answer", answer); + Comment comment = commentService.createReplyCommentByAnswer(commentForm.getCommentContents(), + commentForm.getSecret(), user, answer, parent); + // 부모 댓글이 있는 페이지로 가야하므로, 부모의 페이지를 구해옴 + page = commentService.getPageNumberByAnswer(answer, comment, PAGESIZE); + paging = commentService.getCommentPageByAnswer(page, answer); + model.addAttribute("answerCommentPaging", paging); + // 전체 댓글 수 갱신 + model.addAttribute("totalCount", paging.getTotalElements()); + return "comment/answer_comment :: #answer-comment-list"; + } + } + + // 답글 수정 + 댓글 수정 둘다 + @PreAuthorize("isAuthenticated()") + @PostMapping("/modify/{type}") + public String modify(CommentForm commentForm, Model model, Principal principal, @PathVariable String type) { + Question question = questionService.getQuestion(commentForm.getQuestionId()); + SiteUser user = userService.getUser(principal.getName()); + + if (!(user.isAdmin()) && (user.getId() != commentForm.getCommentWriter())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다"); + } + + // 대댓글(답글)도 id로 찾을 수 있음(댓글과 동일 객체 사용이 가능하니 하나의 메서드로 처리 가능) + Comment comment = commentService.getComment(commentForm.getId()); + + // 댓글 내용, 비밀 댓글 여부만 수정 할테니 해당 값 넘기기 + commentService.modify(comment, commentForm.getCommentContents().trim(), commentForm.getSecret()); + + model.addAttribute("question", question); + + Page paging; + int page=0; + + if (type.equals("question")) { + page = commentService.getPageNumberByQuestion(question, comment, PAGESIZE); + paging = commentService.getCommentPageByQuestion(page, question); + model.addAttribute("questionCommentPaging", paging); + return "comment/question_comment :: #question-comment-list"; + } else { + Answer answer = answerService.getAnswer(commentForm.getAnswerId()); + model.addAttribute("answer", answer); + page = commentService.getPageNumberByAnswer(answer, comment, PAGESIZE); + paging = commentService.getCommentPageByAnswer(page, answer); + model.addAttribute("answerCommentPaging", paging); + return "comment/answer_comment :: #answer-comment-list"; + } + } + + // 댓글 삭제 메서드 + @PreAuthorize("isAuthenticated()") + @PostMapping("/delete/{type}") + public String delete(Model model, CommentForm commentForm, Principal principal, @PathVariable String type) { + Question question = questionService.getQuestion(commentForm.getQuestionId()); + SiteUser user = userService.getUser(principal.getName()); + + // 관리자가 아니거나 현재 로그인한 사용자가 작성한 댓글이 아니면 삭제 불가 + if (!(user.isAdmin()) && (user.getId() != commentForm.getCommentWriter())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제 권한이 없습니다"); + } + + Comment comment = commentService.getComment(commentForm.getId()); + + + // 댓글이 속한 페이지 번호 + int page = 0; + + Answer answer = null; + if (type.equals("question")) { + page = commentService.getPageNumberByQuestion(question, comment, PAGESIZE); + } + + else { + answer = answerService.getAnswer(commentForm.getAnswerId()); + page = commentService.getPageNumberByAnswer(answer, comment, PAGESIZE); + } + + // 부모(댓글)이 있을 경우 연관관계 끊어주기 -> 삭제되더라도 GET 등으로 새로 요청을 보내는 것이 아니기에 + // 이 작업은 꼭 해줘야 대댓글 리스트도 수정된다! + + // 부모댓글이 삭제 되지 않았다면 연관관계 끊어주기만 하면 됨 + // => Ajax 비동기 리스트화를 위해 리스트에서 명시적 삭제 + if (comment.getParent() != null && !comment.getParent().isDeleted()) { + comment.getParent().getChildren().remove(comment); + } + // 부모댓글이 삭제 상태이고 부모의 자식 댓글이 본인 포함 2개 이상이라면 + // 자식 댓글의 삭제가 부모 댓글 객체 삭제에 영향을 주지 않으니 연관관계만 끊어주기 + // => Ajax 비동기 리스트화를 위해 리스트에서 명시적 삭제 + else if (comment.getParent() != null && comment.getParent().isDeleted() + && comment.getParent().getChildren().size() > 1) { + comment.getParent().getChildren().remove(comment); + } + + commentService.delete(comment); + + model.addAttribute("question", question); + Page paging; + + if (type.equals("question")) { + paging = commentService.getCommentPageByQuestion(page, question); + // 만일 삭제 전이 6개 -> 삭제하면 5개 -> 0패이지가 보여져야 하는데, 삭제 전에 page를 계산하여 1페이지가 보여짐. + // 여기서 조건을 검사해줘야 함, 현재 페이지 개수가 0개면 이전 페이지 이동, 단 현재 페이지가 0인데도 개수가 0개? + // 그러면 댓글이 아에 없으니 그냥 0페이지 표기! + // 삭제하기 전에 page를 구한 이유는 댓글이 삭제되면 삭제한 댓글이 원래 어디 페이지에 있는지 검사가 안되기 때문 + if(page !=0 && paging.getNumberOfElements() == 0) + paging = commentService.getCommentPageByQuestion(page-1, question); + model.addAttribute("questionCommentPaging", paging); + // 전체 댓글 수 갱신 + model.addAttribute("totalCount", paging.getTotalElements()); + return "comment/question_comment :: #question-comment-list"; + } else { + paging = commentService.getCommentPageByAnswer(page, answer); + if((page !=0 && paging.getNumberOfElements() == 0)) + paging = commentService.getCommentPageByAnswer(page-1, answer); + + model.addAttribute("answer", answer); + model.addAttribute("answerCommentPaging", paging); + // 전체 댓글 수 갱신 + model.addAttribute("totalCount", paging.getTotalElements()); + return "comment/answer_comment :: #answer-comment-list"; + } + } +} diff --git a/src/main/java/com/ll/spring_additional/boundedContext/comment/entity/Comment.java b/src/main/java/com/ll/spring_additional/boundedContext/comment/entity/Comment.java new file mode 100644 index 0000000..0d2b72a --- /dev/null +++ b/src/main/java/com/ll/spring_additional/boundedContext/comment/entity/Comment.java @@ -0,0 +1,91 @@ +package com.ll.spring_additional.boundedContext.comment.entity; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.ll.spring_additional.boundedContext.answer.entity.Answer; +import com.ll.spring_additional.boundedContext.question.entity.Question; +import com.ll.spring_additional.boundedContext.user.entity.SiteUser; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@Getter +@EntityListeners(AuditingEntityListener.class) +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private SiteUser writer; + + @ManyToOne + private Question question; + + @ManyToOne + private Answer answer; + + @ToString.Exclude + @ManyToOne + private Comment parent; + + // Cascade REMOVE 불가 : 자식 댓글이 있는 상태에서, 그냥 댓글 삭제하면 자식 댓글 전부 지워짐 + // OrphanRemoval로 대댓글과 연관관계 끊어지면 삭제되게 설정 + @OneToMany(mappedBy = "parent", orphanRemoval = true) + @ToString.Exclude + @Builder.Default // 빌더패턴 리스트시 초기화 + private List children = new ArrayList<>(); + + private String content; + + @CreatedDate + private LocalDateTime createDate; + @LastModifiedDate + private LocalDateTime modifyDate; + + @Builder.Default + private Boolean secret = false; + + // 삭제 여부 나타내는 속성 추가 + @Builder.Default + private Boolean deleted = false; + @PrePersist + public void prePersist() { + this.modifyDate = null; + } + + public void deleteParent() { + deleted = true; + } + + // 타임리프에서 비밀 댓글이면 댓글의 내용이 안보이게 하기 위함 + public boolean isSecret() { + return this.secret == true; + } + // 타임리프에서 삭제 댓글이면 댓글의 내용이 안보이게 하기 위함 + public boolean isDeleted() { + return this.deleted == true; + } + +} diff --git a/src/main/java/com/ll/spring_additional/boundedContext/comment/form/CommentForm.java b/src/main/java/com/ll/spring_additional/boundedContext/comment/form/CommentForm.java new file mode 100644 index 0000000..6963731 --- /dev/null +++ b/src/main/java/com/ll/spring_additional/boundedContext/comment/form/CommentForm.java @@ -0,0 +1,30 @@ +package com.ll.spring_additional.boundedContext.comment.form; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class CommentForm { + private Long id; + @NotBlank(message = "내용은 필수항목입니다.") + private String commentContents; + private Integer questionId; + private Integer answerId; + private Boolean secret = false; + private Long parentId; + private Long commentWriter; +} diff --git a/src/main/java/com/ll/spring_additional/boundedContext/comment/repository/CommentRepository.java b/src/main/java/com/ll/spring_additional/boundedContext/comment/repository/CommentRepository.java new file mode 100644 index 0000000..1eb98e3 --- /dev/null +++ b/src/main/java/com/ll/spring_additional/boundedContext/comment/repository/CommentRepository.java @@ -0,0 +1,19 @@ +package com.ll.spring_additional.boundedContext.comment.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.ll.spring_additional.boundedContext.answer.entity.Answer; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; +import com.ll.spring_additional.boundedContext.question.entity.Question; + +public interface CommentRepository extends JpaRepository { + int countByQuestion(Question question); + + int countByAnswer(Answer answer); + + Page findAllByQuestion(Question question, Pageable pageable); + + Page findAllByAnswer(Answer answer, Pageable pageable); +} diff --git a/src/main/java/com/ll/spring_additional/boundedContext/comment/service/CommentService.java b/src/main/java/com/ll/spring_additional/boundedContext/comment/service/CommentService.java new file mode 100644 index 0000000..47553bb --- /dev/null +++ b/src/main/java/com/ll/spring_additional/boundedContext/comment/service/CommentService.java @@ -0,0 +1,202 @@ +package com.ll.spring_additional.boundedContext.comment.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ll.spring_additional.boundedContext.answer.entity.Answer; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; +import com.ll.spring_additional.boundedContext.comment.repository.CommentRepository; +import com.ll.spring_additional.boundedContext.question.entity.Question; +import com.ll.spring_additional.boundedContext.user.entity.SiteUser; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentService { + private final CommentRepository commentRepository; + + @Transactional + public Comment createByQuestion(String content, Boolean secret, SiteUser user, Question question) { + Comment newComment = Comment.builder() + .content(content) + .writer(user) + .question(question) + .secret(secret) + .build(); + // 댓글 생성 + commentRepository.save(newComment); + + return newComment; + } + + // 마지막 페이지 번호 가져오기 + public int getLastPageNumberByQuestion(Question question) { + int commentCount = commentRepository.countByQuestion(question); + int pageSize = 5; // 페이지 당 댓글 수 (조정 가능) + int lastPageNumber = (int)Math.ceil((double)commentCount / pageSize); + // 스프링 0페이지 부터 시작하니 1 빼주기 + // 단 댓글이 1개도 없던 상황일 경우, 댓글이 달린 직후에는 0일테니 -1하면 음수값이 나온다. + // 따라서 음수이면 0을 반환하도록 수정 + if (lastPageNumber - 1 < 0) + return 0; + else + return lastPageNumber - 1; + } + + @Transactional + public Comment createByAnswer(String content, Boolean secret, SiteUser user, Answer answer) { + Comment newComment = Comment.builder() + .content(content) + .writer(user) + .answer(answer) + .secret(secret) + .build(); + // 댓글 생성 + commentRepository.save(newComment); + + return newComment; + + } + + public int getLastPageNumberByAnswer(Answer answer) { + int commentCount = commentRepository.countByAnswer(answer); + int pageSize = 10; // 페이지 당 댓글 수 (조정 가능) + int lastPageNumber = (int)Math.ceil((double)commentCount / pageSize); + // 스프링 0페이지 부터 시작하니 1 빼주기 + // 단 댓글이 1개도 없던 상황일 경우, 댓글이 달린 직후에는 0일테니 -1하면 음수값이 나온다. + // 따라서 음수이면 0을 반환하도록 수정 + if (lastPageNumber - 1 < 0) + return 0; + else + return lastPageNumber - 1; + } + + public Comment getComment(Long commentId) { + return commentRepository.findById(commentId).orElse(null); + } + + @Transactional + public Comment createReplyCommentByQuestion(String commentContents, Boolean secret, SiteUser user, + Question question, Comment parent) { + { + Comment newComment = Comment.builder() + .content(commentContents) + .writer(user) + .question(question) + .secret(secret) + .parent(parent) + .build(); + // 대댓글 저장 + commentRepository.save(newComment); + return newComment; + } + } + + @Transactional + public Comment createReplyCommentByAnswer(String commentContents, Boolean secret, SiteUser user, Answer answer, + Comment parent) { + Comment newComment = Comment.builder() + .content(commentContents) + .writer(user) + .answer(answer) + .secret(secret) + .parent(parent) + .build(); + // 대댓글 저장 + commentRepository.save(newComment); + return newComment; + } + + public Page getCommentPageByQuestion(int page, Question question) { + Pageable pageable = PageRequest.of(page, 5); // 페이지네이션 정보 + return commentRepository.findAllByQuestion(question, pageable); + } + + public Page getCommentPageByAnswer(int page, Answer answer) { + Pageable pageable = PageRequest.of(page, 5); // 페이지네이션 정보 + return commentRepository.findAllByAnswer(answer, pageable); + } + + @Transactional + public void modify(Comment comment, String content, Boolean secret) { + Comment mComment = comment.toBuilder() + .content(content) + .secret(secret) + .build(); + commentRepository.save(mComment); + } + + @Transactional + public void delete(Comment comment) { + if (comment == null) { + throw new IllegalArgumentException("Comment cannot be null"); + } + if (comment.getChildren().size() != 0) { + // 자식이 있으면 삭제 상태만 변경 + comment.deleteParent(); + } else { // 자식이 없다 -> 대댓글이 없다 -> 객체 그냥 삭제해도 된다. + // 삭제 가능한 조상 댓글을 구해서 삭제 + // ex) 할아버지 - 아버지 - 대댓글, 3자라 했을 때 대댓글 입장에서 자식이 없으니 삭제 가능 + // => 삭제하면 아버지도 삭제 가능 => 할아버지도 삭제 가능하니 이런식으로 조상 찾기 메서드 + Comment tmp = getDeletableAncestorComment(comment); + commentRepository.delete(tmp); + } + } + + @Transactional + public Comment getDeletableAncestorComment(Comment comment) { + Comment parent = comment.getParent(); // 현재 댓글의 부모를 구함 + if (parent != null && parent.getChildren().size() == 1 && parent.isDeleted() == true) { + // 부모가 있고, 부모의 자식이 1개(지금 삭제하는 댓글)이고, 부모의 삭제 상태가 TRUE인 댓글이라면 재귀 + // 삭제가능 댓글 -> 만일 댓글의 조상(대댓글의 입장에서 할아버지 댓글)도 해당 댓글 삭제 시 삭제 가능한지 확인 + // 삭제 -> Cascade 옵션으로 가장 부모만 삭제 해도 자식들도 다 삭제 가능 + + // Ajax로 비동기로 리스트 가져오기에, 대댓글 1개인거 삭제할 때 연관관계 삭제하고 부모 댓글 삭제하기 필요 + // 컨트롤러가 아닌 서비스의 삭제에서 처리해주는 이유는 연관관계를 삭제해주면 parent를 구할 수 없기에 여기서 끊어줘야 함 + // 연관관계만 끊어주면 orphanRemoval 옵션으로 자식 객체는 삭제되니 부모를 삭제 대상으로 넘기면 됨 + parent.getChildren().remove(comment); + + return getDeletableAncestorComment(parent); + } + + return comment; + } + + public int getPageNumberByQuestion(Question question, Comment comment, int pageSize) { + // 자식 댓글이면, 부모 댓글이 원래 있던 페이지 번호를 구해야 하므로 변경 + if(comment.getParent() != null) { + comment = comment.getParent(); + } + List commentList = question.getComments(); + int index = commentList.indexOf(comment); + + if (index == -1) { + throw new IllegalArgumentException("해당 댓글이 존재하지 않습니다."); + } + + return index / pageSize; + } + + public int getPageNumberByAnswer(Answer answer, Comment comment, int pageSize) { + // 자식 댓글이면, 부모 댓글이 원래 있던 페이지 번호를 구해야 하므로 변경 + if(comment.getParent() != null) { + comment = comment.getParent(); + } + List commentList = answer.getComments(); + int index = commentList.indexOf(comment); + + if (index == -1) { + throw new IllegalArgumentException("해당 댓글이 존재하지 않습니다."); + } + + return index / pageSize; + } +} diff --git a/src/main/java/com/ll/spring_additional/boundedContext/question/controller/QuestionController.java b/src/main/java/com/ll/spring_additional/boundedContext/question/controller/QuestionController.java index dce5f32..4b17a92 100644 --- a/src/main/java/com/ll/spring_additional/boundedContext/question/controller/QuestionController.java +++ b/src/main/java/com/ll/spring_additional/boundedContext/question/controller/QuestionController.java @@ -1,6 +1,9 @@ package com.ll.spring_additional.boundedContext.question.controller; import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.HashMap; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; @@ -20,6 +23,8 @@ import com.ll.spring_additional.boundedContext.answer.entity.Answer; import com.ll.spring_additional.boundedContext.answer.form.AnswerForm; import com.ll.spring_additional.boundedContext.answer.service.AnswerService; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; +import com.ll.spring_additional.boundedContext.comment.service.CommentService; import com.ll.spring_additional.boundedContext.question.entity.Question; import com.ll.spring_additional.boundedContext.question.form.QuestionForm; import com.ll.spring_additional.boundedContext.question.questionEnum.QuestionEnum; @@ -38,6 +43,8 @@ public class QuestionController { private final UserService userService; private final AnswerService answerService; + private final CommentService commentService; + @GetMapping("/list/{type}") public String list(Model model, @PathVariable String type, @RequestParam(value = "page", defaultValue = "0") int page @@ -101,9 +108,11 @@ public String personalListByAnswerUserId(Model model, @PathVariable Long id, public String detail(Model model, @PathVariable Integer id, AnswerForm answerForm, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "") String sort) { Question question = questionService.getQuestion(id); - Page paging = answerService.getAnswerPage(question, page, sort); model.addAttribute("question", question); + + Page paging = answerService.getAnswerPage(question, page, sort); model.addAttribute("paging", paging); + return "question/question_detail"; } diff --git a/src/main/java/com/ll/spring_additional/boundedContext/question/entity/Question.java b/src/main/java/com/ll/spring_additional/boundedContext/question/entity/Question.java index dd95d8d..835d350 100644 --- a/src/main/java/com/ll/spring_additional/boundedContext/question/entity/Question.java +++ b/src/main/java/com/ll/spring_additional/boundedContext/question/entity/Question.java @@ -13,6 +13,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import com.ll.spring_additional.boundedContext.answer.entity.Answer; +import com.ll.spring_additional.boundedContext.comment.entity.Comment; import com.ll.spring_additional.boundedContext.question.questionEnum.QuestionEnum; import com.ll.spring_additional.boundedContext.user.entity.SiteUser; @@ -29,6 +30,7 @@ import jakarta.persistence.PrePersist; import lombok.Getter; import lombok.Setter; +import lombok.ToString; @Getter @Setter @@ -107,5 +109,8 @@ public String getCategoryAsString() { } } + @OneToMany(mappedBy = "question", cascade = {CascadeType.REMOVE}) + @ToString.Exclude + private List comments = new ArrayList<>(); } diff --git a/src/main/java/com/ll/spring_additional/boundedContext/user/entity/SiteUser.java b/src/main/java/com/ll/spring_additional/boundedContext/user/entity/SiteUser.java index 70fbf6f..4dedf99 100644 --- a/src/main/java/com/ll/spring_additional/boundedContext/user/entity/SiteUser.java +++ b/src/main/java/com/ll/spring_additional/boundedContext/user/entity/SiteUser.java @@ -30,4 +30,8 @@ public class SiteUser { public String getJdenticon() { return Ut.hash.sha256(this.username); } + + public boolean isAdmin() { + return this.username.equals("admin"); + } } \ No newline at end of file diff --git a/src/main/java/com/ll/spring_additional/boundedContext/user/service/UserService.java b/src/main/java/com/ll/spring_additional/boundedContext/user/service/UserService.java index dd36c9c..72bb28f 100644 --- a/src/main/java/com/ll/spring_additional/boundedContext/user/service/UserService.java +++ b/src/main/java/com/ll/spring_additional/boundedContext/user/service/UserService.java @@ -47,7 +47,7 @@ public SiteUser getUser(String username) { if (siteUser.isPresent()) { return siteUser.get(); } else { - throw new DataNotFoundException("siteuser not found"); + throw new DataNotFoundException("site user not found"); } } diff --git a/src/main/resources/templates/comment/answer_comment.html b/src/main/resources/templates/comment/answer_comment.html new file mode 100644 index 0000000..4d38ed1 --- /dev/null +++ b/src/main/resources/templates/comment/answer_comment.html @@ -0,0 +1,651 @@ +
+ +
+
+
+ +
+
+
+ + + +
+ +
+ + +
+ + +
0
+
    + +
+ +
+ +
+ + + + + + +
\ No newline at end of file diff --git a/src/main/resources/templates/comment/question_comment.html b/src/main/resources/templates/comment/question_comment.html new file mode 100644 index 0000000..69ec585 --- /dev/null +++ b/src/main/resources/templates/comment/question_comment.html @@ -0,0 +1,640 @@ +
+ +
+
+
+ +
+
+
+ + + +
+
+ +
+ + +
0
+ +
    + +
+ +
+ +
+ +
+ + + + + +
\ No newline at end of file diff --git a/src/main/resources/templates/common/layout.html b/src/main/resources/templates/common/layout.html index d874f0c..f7f85f8 100644 --- a/src/main/resources/templates/common/layout.html +++ b/src/main/resources/templates/common/layout.html @@ -25,6 +25,16 @@ color:inherit; text-decoration:inherit; } + + ul, li { + /* 앞에 점 없애기 */ + list-style:none; + /* 안쪽 여백 제거 */ + padding:0; + /* 바깥쪽 여백 제거 */ + margin:0; + } + diff --git a/src/main/resources/templates/question/question_detail.html b/src/main/resources/templates/question/question_detail.html index 17a37fb..40ddb98 100644 --- a/src/main/resources/templates/question/question_detail.html +++ b/src/main/resources/templates/question/question_detail.html @@ -41,11 +41,21 @@

+ +
-
+
+ + @@ -190,6 +211,62 @@

}); } } + + // 질문에 달린 댓글 보여주는 메서드 + const showQuestionComment = () => { + const questionId = [[${question.id}]]; + const qCommentList = $('#q-comment-list'); + // 댓글이 이미 표시되어 있다면 숨김 + if (qCommentList.is(':visible')) { + qCommentList.hide(); + } else { + // 댓글이 숨겨져 있을 경우만 Ajax 요청 + $.ajax({ + // 요청방식: get, 요청주소: /comment/question/id + // 요청데이터: 작성내용, 게시글번호, 비밀 댓글 여부, 부모 댓글 id + type: "get", + url: "/comment/question/" + questionId, + data: { + }, + success: function (fragment) { + qCommentList.show(); + qCommentList.html(fragment); + }, + error: function (err) { + console.log("요청 실패", err); + } + }); + } + } + + // 답변에 달린 댓글 보여주는 메서드 + const showAnswerComment = (answerIndex) => { + const questionId = [[${question.id}]]; + const aCommentList = $("#a-comment-list-" + answerIndex); + // 댓글이 이미 표시되어 있다면 숨김 + if (aCommentList.is(':visible')) { + aCommentList.hide(); + } else { + // 댓글이 숨겨져 있을 경우만 Ajax 요청 + $.ajax({ + // 요청방식: get, 요청주소: /comment/answer/id + // 요청데이터: 작성내용, 게시글번호, 비밀 댓글 여부, 부모 댓글 id + type: "get", + url: "/comment/answer/" + answerIndex, + data: { + "questionId" : questionId + }, + success: function (fragment) { + aCommentList.show(); + aCommentList.html(fragment); + }, + error: function (err) { + console.log("요청 실패", err); + } + }); + } + } + // 마크다운에디터 var simplemde = new SimpleMDE({element: document.getElementById("content")});