Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#83 댓글 대댓글 crud api 구현 #97

Merged
merged 25 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
629a086
feat : Comment 생성 API 구현
java-saeng Jul 24, 2023
c05f146
docs : Comment http 문서 작성
java-saeng Jul 24, 2023
96eb475
refactor : CommentResponse에서 Comment를 파라미터로 받아서 Response 로 만들어주는 정적 팩…
java-saeng Jul 24, 2023
328e140
Merge branch 'backend-main' of https://github.com/woowacourse-teams/2…
java-saeng Jul 25, 2023
164ce3e
refactor : 이전 커밋 pull 하면서 EventRepository 패키지 변경으로 인한 import 변경
java-saeng Jul 25, 2023
3fcd4b5
refactor : 정적 팩토리 추가로 인해 private 기본 생성자 추가 및 테스트를 위한 모든 파라미터를 가진 생성자 추가
java-saeng Jul 25, 2023
9f878ed
feat : 행사에 존재하는 댓글 모두 조회하는 API 구현
java-saeng Jul 25, 2023
4816a5a
fix : @DataJpaTest 관련 테스트가 모두 실패하는 것을 data.sql 을 매 테스트 메서드마다 실행하는 것으로 해결
java-saeng Jul 25, 2023
dcd52a4
feat : 문서화 테스트
java-saeng Jul 25, 2023
3a0042d
feat : http 테스트 추가
java-saeng Jul 25, 2023
29b9788
fix : AuditingEntityListener 때문에 테스트에서 문제 해결
java-saeng Jul 25, 2023
fce3a79
feat : Comment 삭제 API 구현
java-saeng Jul 25, 2023
68c52ed
feat : Comment 삭제 API 문서 추가
java-saeng Jul 25, 2023
d090c86
feat : Comment 삭제 http 테스트 추가
java-saeng Jul 25, 2023
6a316eb
feat : 댓글 수정 API 구현
java-saeng Jul 25, 2023
4596168
feat : 댓글 수정 API rest docs
java-saeng Jul 25, 2023
ee38f8a
feat : 댓글 수정 API http 테스트 추가
java-saeng Jul 25, 2023
df00603
feat : 댓글 정보 반환 시 댓글 작성자의 이름, imageUrl, 아이디 반환
java-saeng Jul 26, 2023
1684186
fix : conflict 해결 및 pull backend-main
java-saeng Jul 26, 2023
e6a96e5
fix : conflict 해결 및 pull backend-main
java-saeng Jul 26, 2023
e8f03fa
test : 모든 테스트에서 사용하는 변수 인스턴스 필드로 빼기
java-saeng Jul 26, 2023
404a370
test : auto increment 로 인해 테스트 깨질 수 있는 것을 예방하기 위해 생성된 이벤트의 아이디를 사용
java-saeng Jul 26, 2023
98f0b0d
refactor : FORBIDDEN일 경우 exceptionType 변경
java-saeng Jul 26, 2023
c29ed29
refactor : 부모, 자식 Comment 생성 시 정적 팩터리 메서드 사용
java-saeng Jul 26, 2023
cb1fd05
refactor : Comment에서 Parent 가 null일 수 있으므로 getter에 Optional 추가
java-saeng Jul 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions backend/emm-sale/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,58 @@ include::{snippets}/find-event/http-response.adoc[]

.HTTP response 설명
include::{snippets}/find-event/response-fields.adoc[]

== Comment

=== `GET` : 댓글 모두 조회

.HTTP request 설명
include::{snippets}/get-comment/request-parameters.adoc[]

.HTTP request
include::{snippets}/get-comment/http-request.adoc[]

.HTTP response
include::{snippets}/get-comment/http-response.adoc[]

.HTTP response 설명
include::{snippets}/get-comment/response-fields.adoc[]

=== `POST` : 댓글 및 대댓글 생성

.HTTP request
include::{snippets}/add-comment/http-request.adoc[]

.HTTP response
include::{snippets}/add-comment/http-response.adoc[]

.HTTP response 설명
include::{snippets}/add-comment/response-fields.adoc[]

=== `DELETE` : 댓글 삭제

.HTTP request
include::{snippets}/delete-comment/http-request.adoc[]

.HTTP request 설명
include::{snippets}/delete-comment/path-parameters.adoc[]

.HTTP response
include::{snippets}/delete-comment/http-response.adoc[]

=== `PATCH` : 댓글 수정

.HTTP request
include::{snippets}/modify-comment/http-request.adoc[]

.HTTP request 설명
include::{snippets}/modify-comment/request-fields.adoc[]

.PathVariable 설명
include::{snippets}/modify-comment/path-parameters.adoc[]

.HTTP response
include::{snippets}/modify-comment/http-response.adoc[]

.HTTP response 설명
include::{snippets}/modify-comment/response-fields.adoc[]
11 changes: 9 additions & 2 deletions backend/emm-sale/src/main/java/com/emmsale/base/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@
@MappedSuperclass
public abstract class BaseEntity {

@Column(updatable = false, nullable = false)
@Column(updatable = false)
@CreatedDate
private LocalDateTime createdAt;
@Column(nullable = false)
@LastModifiedDate
private LocalDateTime updatedAt;

public LocalDateTime getCreatedAt() {
return createdAt;
}

public LocalDateTime getUpdatedAt() {
return updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.emmsale.comment.api;

import com.emmsale.comment.application.CommentCommandService;
import com.emmsale.comment.application.CommentQueryService;
import com.emmsale.comment.application.dto.CommentAddRequest;
import com.emmsale.comment.application.dto.CommentHierarchyResponse;
import com.emmsale.comment.application.dto.CommentModifyRequest;
import com.emmsale.comment.application.dto.CommentResponse;
import com.emmsale.member.domain.Member;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class CommentApi {

private final CommentCommandService commentCommandService;
private final CommentQueryService commentQueryService;

@PostMapping("/comments")
public CommentResponse create(
@RequestBody final CommentAddRequest commentAddRequest,
final Member member
) {
return commentCommandService.create(commentAddRequest, member);
}

@GetMapping("/comments")
public List<CommentHierarchyResponse> findAll(@RequestParam final Long eventId) {
return commentQueryService.findAllCommentsByEventId(eventId);
}

@DeleteMapping("/comments/{comment-id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable("comment-id") final Long commentId, final Member member) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 개인적으로 궁금해서 여쭤보는건데 혹시 comment-id로 받으시는 이유가 있으신가요?
commentId로 받으면 PathVariable에 name을 생략할 수 있는데 굳이 comment-id로 받아서 name을 명시한 이유가 있는지 궁금합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현장 리뷰 완료요~~

commentCommandService.delete(commentId, member);
}

@PatchMapping("/comments/{comment-id}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment 내용을 완전히 덮어씌우는 거라면
Put 은 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현장 리뷰 완료요~~

public CommentResponse modify(
@PathVariable("comment-id") final Long commentId,
final Member member,
@RequestBody final CommentModifyRequest commentModifyRequest
) {
return commentCommandService.modify(commentId, member, commentModifyRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.emmsale.comment.application;

import static com.emmsale.comment.exception.CommentExceptionType.FORBIDDEN_DELETE_COMMENT;
import static com.emmsale.comment.exception.CommentExceptionType.FORBIDDEN_MODIFY_COMMENT;
import static com.emmsale.comment.exception.CommentExceptionType.FORBIDDEN_MODIFY_DELETED_COMMENT;
import static com.emmsale.comment.exception.CommentExceptionType.NOT_FOUND_COMMENT;

import com.emmsale.comment.application.dto.CommentAddRequest;
import com.emmsale.comment.application.dto.CommentModifyRequest;
import com.emmsale.comment.application.dto.CommentResponse;
import com.emmsale.comment.domain.Comment;
import com.emmsale.comment.domain.CommentRepository;
import com.emmsale.comment.exception.CommentException;
import com.emmsale.comment.exception.CommentExceptionType;
import com.emmsale.event.domain.Event;
import com.emmsale.event.domain.repository.EventRepository;
import com.emmsale.event.exception.EventException;
import com.emmsale.event.exception.EventExceptionType;
import com.emmsale.member.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class CommentCommandService {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 Command가 나타내는게 Query 이외의 모든 동작인가요? 지금은 Query와 Command밖에 없어서 쉽게 예측이 되지만 더 늘어난다면 Command에 어떤 동작이 있는지 예측하기 어려울 것 같아요.
혹시 CommentWriteService, CommentDeleteService 이런 식으로 나누는건 어떠신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현장 리뷰 완료요~~


private final CommentRepository commentRepository;
private final EventRepository eventRepository;

public CommentResponse create(
final CommentAddRequest commentAddRequest,
final Member member
) {
final Event savedEvent = eventRepository.findById(commentAddRequest.getEventId())
.orElseThrow(() -> new EventException(EventExceptionType.EVENT_NOT_FOUND_EXCEPTION));
final String content = commentAddRequest.getContent();

final Comment comment = commentAddRequest.optionalParentId()
.map(commentId -> Comment.createChild(
savedEvent,
findSavedComment(commentId),
member,
content)
)
.orElseGet(() -> Comment.createRoot(savedEvent, member, content));

return CommentResponse.from(commentRepository.save(comment));
}

private Comment findSavedComment(final Long commentId) {
return commentRepository.findById(commentId)
.orElseThrow(() -> new CommentException(NOT_FOUND_COMMENT));
}

public void delete(final Long commentId, final Member loginMember) {

final Comment comment = findSavedComment(commentId);

validateSameWriter(loginMember, comment, FORBIDDEN_DELETE_COMMENT);

comment.delete();
}

private void validateSameWriter(
final Member loginMember,
final Comment comment,
final CommentExceptionType commentExceptionType
) {
if (loginMember.isNotMe(comment.getMember())) {
throw new CommentException(commentExceptionType);
}
}

public CommentResponse modify(
final Long commentId,
final Member loginMember,
final CommentModifyRequest commentModifyRequest
) {

final Comment comment = findSavedComment(commentId);

validateAlreadyDeleted(comment);
validateSameWriter(loginMember, comment, FORBIDDEN_MODIFY_COMMENT);

comment.modify(commentModifyRequest.getContent());

return CommentResponse.from(comment);
}

private void validateAlreadyDeleted(final Comment comment) {
if (comment.isDeleted()) {
throw new CommentException(FORBIDDEN_MODIFY_DELETED_COMMENT);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.emmsale.comment.application;

import com.emmsale.comment.application.dto.CommentHierarchyResponse;
import com.emmsale.comment.domain.Comment;
import com.emmsale.comment.domain.CommentRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentQueryService {

private final CommentRepository commentRepository;

public List<CommentHierarchyResponse> findAllCommentsByEventId(final Long eventId) {

final List<Comment> comments = commentRepository.findByEventId(eventId);

return CommentHierarchyResponse.from(comments);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.emmsale.comment.application.dto;

import java.util.Optional;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class CommentAddRequest {

private final String content;
private final Long eventId;
private final Long parentId;

public Optional<Long> optionalParentId() {
return Optional.ofNullable(parentId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.emmsale.comment.application.dto;

import com.emmsale.base.BaseEntity;
import com.emmsale.comment.domain.Comment;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class CommentHierarchyResponse {

private final CommentResponse parentComment;
private final List<CommentResponse> childComments;

public static List<CommentHierarchyResponse> from(final List<Comment> comments) {

final Map<Comment, List<Comment>> groupedByParent =
groupingByParentAndSortedByCreatedAt(comments);

final List<CommentHierarchyResponse> result = new ArrayList<>();

for (final Entry<Comment, List<Comment>> entry : groupedByParent.entrySet()) {
final Comment parentComment = entry.getKey();
final List<CommentResponse> childCommentResponses =
mapToCommentResponse(entry, parentComment);

result.add(
new CommentHierarchyResponse(CommentResponse.from(parentComment), childCommentResponses)
);
}

return result;
}

private static LinkedHashMap<Comment, List<Comment>> groupingByParentAndSortedByCreatedAt(
final List<Comment> comments
) {
return comments.stream()
.sorted(Comparator.comparing(BaseEntity::getCreatedAt))
.collect(Collectors.groupingBy(
it -> it.getParent().orElse(it),
LinkedHashMap::new, Collectors.toList())
);
}

private static List<CommentResponse> mapToCommentResponse(
final Entry<Comment, List<Comment>> entry,
final Comment parentComment
) {
return entry.getValue().stream()
.filter(it -> isNotSameKeyAndValue(parentComment, it))
.map(CommentResponse::from)
.sorted(Comparator.comparing(CommentResponse::getCreatedAt))
.collect(Collectors.toList());
}

private static boolean isNotSameKeyAndValue(final Comment parentComment, final Comment target) {
return !target.equals(parentComment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.emmsale.comment.application.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class CommentModifyRequest {

private final String content;

private CommentModifyRequest() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이게 왜 필요한지 알 수 있을까요?
없으면 테스트가 터지는데 다른 Request DTO들은 없어도 되는데 이 친구만 특별하게 있어야 하는 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현장 리뷰 완료요~~

this(null);
}
}
Loading
Loading