diff --git a/src/main/java/codesquad/QnaApplication.java b/src/main/java/codesquad/QnaApplication.java index 6d19f8aa..8959ef06 100644 --- a/src/main/java/codesquad/QnaApplication.java +++ b/src/main/java/codesquad/QnaApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class QnaApplication { public static void main(String[] args) { diff --git a/src/main/java/codesquad/domain/BaseTimeEntity.java b/src/main/java/codesquad/domain/BaseTimeEntity.java new file mode 100644 index 00000000..bed7895a --- /dev/null +++ b/src/main/java/codesquad/domain/BaseTimeEntity.java @@ -0,0 +1,20 @@ +package codesquad.domain; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdDate; + + public LocalDateTime getCreatedDate() { + return createdDate; + } +} diff --git a/src/main/java/codesquad/domain/Result.java b/src/main/java/codesquad/domain/Result.java new file mode 100644 index 00000000..3e64cebe --- /dev/null +++ b/src/main/java/codesquad/domain/Result.java @@ -0,0 +1,28 @@ +package codesquad.domain; + +public class Result { + + private T data; + private String errorMessage; + + private Result(T data, String errorMessage) { + this.data = data; + this.errorMessage = errorMessage; + } + + public static Result ok(T data) { + return new Result<>(data, null); + } + + public static Result fail(String errorMessage) { + return new Result<>(null, errorMessage); + } + + public T getData() { + return data; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/codesquad/domain/answer/Answer.java b/src/main/java/codesquad/domain/answer/Answer.java index 42498777..8eb4d1da 100644 --- a/src/main/java/codesquad/domain/answer/Answer.java +++ b/src/main/java/codesquad/domain/answer/Answer.java @@ -1,12 +1,13 @@ package codesquad.domain.answer; +import codesquad.domain.BaseTimeEntity; import codesquad.domain.question.Question; import codesquad.domain.user.User; import javax.persistence.*; @Entity -public class Answer { +public class Answer extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -15,14 +16,25 @@ public class Answer { @JoinColumn(foreignKey = @ForeignKey(name = "fk_answer_writer")) private User writer; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(foreignKey = @ForeignKey(name = "fk_answer_to_question")) private Question question; @Column(columnDefinition = "TEXT", length = 300, nullable = false) private String contents; - private Boolean isDeleted = false; + @Column(columnDefinition = "boolean default false") + private boolean deleted = false; + + public Answer() { + + } + + public Answer(User writer, Question question, String contents) { + this.writer = writer; + this.question = question; + this.contents = contents; + } public Long getId() { return id; @@ -56,8 +68,8 @@ public void setContents(String contents) { this.contents = contents; } - public Boolean getDeleted() { - return isDeleted; + public boolean isDeleted() { + return deleted; } public boolean isSameWriter(User user) { @@ -65,6 +77,6 @@ public boolean isSameWriter(User user) { } public void delete() { - isDeleted = true; + deleted = true; } } diff --git a/src/main/java/codesquad/domain/answer/AnswerRepository.java b/src/main/java/codesquad/domain/answer/AnswerRepository.java index 8c2e8f87..6d4fb3de 100644 --- a/src/main/java/codesquad/domain/answer/AnswerRepository.java +++ b/src/main/java/codesquad/domain/answer/AnswerRepository.java @@ -1,6 +1,12 @@ package codesquad.domain.answer; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AnswerRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"question"}) + Optional findQuestionFetchJoinById(Long id); } diff --git a/src/main/java/codesquad/domain/question/Question.java b/src/main/java/codesquad/domain/question/Question.java index 06a6aedb..b85291ee 100644 --- a/src/main/java/codesquad/domain/question/Question.java +++ b/src/main/java/codesquad/domain/question/Question.java @@ -1,13 +1,15 @@ package codesquad.domain.question; +import codesquad.domain.BaseTimeEntity; import codesquad.domain.answer.Answer; import codesquad.domain.user.User; +import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.*; import java.util.List; @Entity -public class Question { +public class Question extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long index; @@ -23,11 +25,13 @@ public class Question { private String contents; @OneToMany(mappedBy = "question") + @JsonIgnore private List answers; private Integer countOfAnswer = 0; - private Boolean isDeleted = false; + @Column(columnDefinition = "boolean default false") + private boolean deleted = false; public Long getIndex() { return index; @@ -99,11 +103,18 @@ public void deleteAnswer() { } public void delete() { - isDeleted = true; + deleted = true; + } + + public boolean isDeleted() { + return deleted; } public boolean canDelete() { - if (countOfAnswer.equals(0)) { + boolean isAnswerStatusAllTrue = answers.stream() + .allMatch(answer -> answer.isDeleted() == true); + + if (countOfAnswer.equals(0) || isAnswerStatusAllTrue) { return true; } return false; diff --git a/src/main/java/codesquad/domain/user/User.java b/src/main/java/codesquad/domain/user/User.java index ecf23768..20cb46f0 100644 --- a/src/main/java/codesquad/domain/user/User.java +++ b/src/main/java/codesquad/domain/user/User.java @@ -1,9 +1,11 @@ package codesquad.domain.user; +import codesquad.domain.BaseTimeEntity; + import javax.persistence.*; @Entity -public class User { +public class User extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/codesquad/web/ApiAnswerController.java b/src/main/java/codesquad/web/ApiAnswerController.java new file mode 100644 index 00000000..1e8ebdfd --- /dev/null +++ b/src/main/java/codesquad/web/ApiAnswerController.java @@ -0,0 +1,65 @@ +package codesquad.web; + +import codesquad.domain.Result; +import codesquad.domain.answer.Answer; +import codesquad.domain.answer.AnswerRepository; +import codesquad.domain.question.Question; +import codesquad.domain.question.QuestionRepository; +import codesquad.domain.user.User; +import codesquad.util.HttpSessionUtil; +import codesquad.web.dto.AnswerSaveRequestDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpSession; +import java.util.NoSuchElementException; + +@RestController +@RequestMapping("/api/questions/{index}/answers") +public class ApiAnswerController { + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private AnswerRepository answerRepository; + + @PostMapping("") + public Answer create(@PathVariable Long index, @RequestBody AnswerSaveRequestDto answerSaveRequestDto, HttpSession httpSession) { + if(!HttpSessionUtil.isLoginUser(httpSession)) { + return null; + } + + User sessionedUser = HttpSessionUtil.getUserFrom(httpSession); + Question savedQuestion = questionRepository.findById(index).orElseThrow(() -> new NoSuchElementException("게시글이 존재하지 않습니다.")); + Answer answer = new Answer(sessionedUser, savedQuestion, answerSaveRequestDto.getContents()); + savedQuestion.addCountOfAnswer(); + + return answerRepository.save(answer); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long index, @PathVariable Long id, HttpSession httpSession) { + if(!HttpSessionUtil.isLoginUser(httpSession)) { + return Result.fail("세션이 만료되었습니다. 다시 로그인 해주세요."); + } + + Answer savedAnswer = answerRepository.findQuestionFetchJoinById(id).orElseThrow(() -> new NoSuchElementException("답변이 존재하지 않습니다.")); + User sessionedUser = HttpSessionUtil.getUserFrom(httpSession); + + if (!savedAnswer.isSameWriter(sessionedUser)) { + return Result.fail("다른 사람의 게시글을 삭제할 수 없습니다."); + } + + savedAnswer.delete(); + + answerRepository.save(savedAnswer); + + Question savedQuestion = questionRepository.findById(index).orElseThrow(() -> new NoSuchElementException("게시글이 존재하지 않습니다.")); + savedQuestion.deleteAnswer(); + + questionRepository.save(savedQuestion); + + return Result.ok(savedAnswer); + } +} diff --git a/src/main/java/codesquad/web/QuestionController.java b/src/main/java/codesquad/web/QuestionController.java index c8df6e0c..1abfe79a 100644 --- a/src/main/java/codesquad/web/QuestionController.java +++ b/src/main/java/codesquad/web/QuestionController.java @@ -134,7 +134,7 @@ public String delete(@PathVariable Long index, HttpSession httpSession, Redirect } if (!savedQuestion.canDelete()) { - throw new IllegalStateException("질문에 다른 사용자의 답변이 존재하여 질문을 삭제할 수 없습니다."); + throw new IllegalStateException("질문을 삭제할 수 없습니다."); } savedQuestion.delete(); diff --git a/src/main/java/codesquad/web/dto/AnswerSaveRequestDto.java b/src/main/java/codesquad/web/dto/AnswerSaveRequestDto.java new file mode 100644 index 00000000..dbf7c780 --- /dev/null +++ b/src/main/java/codesquad/web/dto/AnswerSaveRequestDto.java @@ -0,0 +1,10 @@ +package codesquad.web.dto; + +public class AnswerSaveRequestDto { + + private String contents; + + public String getContents() { + return contents; + } +} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 4538cadd..df491baf 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,2 +1,4 @@ -INSERT INTO USER (id, user_id, password, name, email) VALUES (1, 'javajigi', 'test', '자바지기', 'javajigi@slipp.net'); -INSERT INTO USER (id, user_id, password, name, email) VALUES (2, 'sanjigi', 'test', '산지기', 'sanjigi@slipp.net'); \ No newline at end of file +INSERT INTO USER (id, created_date, user_id, password, name, email) VALUES (1, '2022-04-18 16:20:29.311', 'javajigi', 'test', '자바지기', 'javajigi@slipp.net'); +INSERT INTO USER (id, created_date, user_id, password, name, email) VALUES (2, '2022-04-18 16:20:29.311', 'sanjigi', 'test', '산지기', 'sanjigi@slipp.net'); + +INSERT INTO QUESTION(index, created_date, contents, count_of_answer, deleted, title, writer_id) VALUES(1, '2022-04-19 16:20:29.311', '자동차 경주 게임을 수행하세요.', 0, false, '오늘의 미션은?', 1); \ No newline at end of file diff --git a/src/main/resources/static/js/scripts.js b/src/main/resources/static/js/scripts.js index 01f85bd2..2555b9c3 100755 --- a/src/main/resources/static/js/scripts.js +++ b/src/main/resources/static/js/scripts.js @@ -6,4 +6,102 @@ String.prototype.format = function() { : match ; }); -}; \ No newline at end of file +}; + +function $(selector) { + return document.querySelector(selector); +} + +document.addEventListener("DOMContentLoaded", () => { + initEvents(); +}) + +function initEvents() { + const answerBtn = $(".submit-write .btn"); + + console.log(answerBtn); + answerBtn.addEventListener("click", registerAnswerHandler); + $('.qna-comment-slipp-articles').addEventListener('click',deleteAnswerHandler); +} + +function fetchManager({ url, method, body, headers, callback}) { + fetch(url, {method,body,headers,credentials: "same-origin"}) + .then((response) => { + return response.json() + }).then((result) => { + callback(result) + }) +} + +function registerAnswerHandler(evt) { + evt.preventDefault(); + const contents = $(".submit-write textarea").value; + $(".submit-write textarea").value = ""; + const url = $(".submit-write").action; + + fetchManager({ + url: url, + method: 'POST', + headers: { 'content-type': 'application/json'}, + body: JSON.stringify({contents}), + callback: appendAnswer + }) +} + +function appendAnswer({id, contents, question, writer, createdDate}) { + const html = ` +
+
+
+ +
+
+ +
${createdDate}
+
+
+
+ ${contents} +
+
+
    +
  • + 수정 +
  • +
  • +
    + + +
    +
  • +
+
+
` + + $('.qna-comment-slipp-articles').insertAdjacentHTML('afterbegin', html); +} + +function deleteAnswerHandler(evt) { + console.log(evt.target.className); + if(evt.target.className !== "delete-answer-button") return; + evt.preventDefault(); + + const url = $(".delete-answer-form").action; + console.log(url); + + fetchManager({ + url: url, + method: 'DELETE', + headers: { 'content-type': 'application/json'}, + callback: deleteAnswer + }) +} + +function deleteAnswer({data}) { + console.log(data); + console.log(data.id); + const target = $(`.article-comment[data-id='${data.id}']`); + console.log(target); + console.log(target.parentNode); + target.parentNode.removeChild(target); +} \ No newline at end of file diff --git a/src/main/resources/templates/failed.html b/src/main/resources/templates/failed.html deleted file mode 100644 index 64e90c12..00000000 --- a/src/main/resources/templates/failed.html +++ /dev/null @@ -1,3 +0,0 @@ -{{>layout/header}} -

수정할 수 없습니다.

-{{>layout/footer}} \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 115ba412..1f09f607 100755 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -5,7 +5,7 @@
    {{#questions}} - {{^isDeleted}} + {{^deleted}}
  • @@ -14,7 +14,7 @@
    - 2016-01-15 18:47 + {{createdDate}} {{writer.name}}
    @@ -24,7 +24,7 @@
  • - {{/isDeleted}} + {{/deleted}} {{/questions}}
diff --git a/src/main/resources/templates/layout/footer.html b/src/main/resources/templates/layout/footer.html index 0cc611b1..b26824cb 100644 --- a/src/main/resources/templates/layout/footer.html +++ b/src/main/resources/templates/layout/footer.html @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/src/main/resources/templates/qna/show.html b/src/main/resources/templates/qna/show.html index ef8f8ec4..1f7ee0a0 100755 --- a/src/main/resources/templates/qna/show.html +++ b/src/main/resources/templates/qna/show.html @@ -16,7 +16,7 @@

{{title}}

- 2015-12-30 01:47 + {{createdDate}}
@@ -54,8 +54,8 @@

{{title}}

{{countOfAnswer}}개의 의견

{{#answers}} - {{^isDeleted}} -
- - {{>layout/footer}} \ No newline at end of file