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

댓글 조회 기능 구현 #182

Merged
merged 10 commits into from
Aug 2, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public LoginResponse register(final String code) {
final Member member = Member.from(response);
final Member registeredMember = memberService.register(member);
final String token = tokenProcessor.generateToken(registeredMember);
return new LoginResponse(token, registeredMember.getNickname().getValue());
return new LoginResponse(token, registeredMember.getNickname());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,8 @@ public void changeNickname(final String nickname) {
this.nickname = new Nickname(nickname);
}

public String getNickname() {
return nickname.getValue();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public MemberInfoResponse findMemberInfo(final Member member) {
final int numberOfVotes = voteRepository.countByMember(member);

return new MemberInfoResponse(
member.getNickname().getValue(),
member.getNickname(),
member.getPoint(),
numberOfPosts,
numberOfVotes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.dto.request.CommentUpdateRequest;
import com.votogether.domain.post.dto.response.CommentResponse;
import com.votogether.domain.post.service.PostCommentService;
import com.votogether.exception.ExceptionResponse;
import com.votogether.global.jwt.Auth;
Expand All @@ -14,10 +15,12 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
Expand Down Expand Up @@ -52,6 +55,21 @@ public ResponseEntity<Void> createComment(
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@Operation(summary = "게시글 댓글 목록 조회", description = "게시글 댓글 목록을 조회한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시글 댓글 목록 조회 성공"),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 게시글",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
)
})
@GetMapping("/{postId}/comments")
public ResponseEntity<List<CommentResponse>> getComments(@PathVariable final Long postId) {
List<CommentResponse> response = postCommentService.getComments(postId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2
missing final keyword

return ResponseEntity.ok(response);
}

@Operation(summary = "게시글 댓글 수정", description = "게시글 댓글을 수정한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시글 댓글 수정 성공"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.votogether.domain.post.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.entity.comment.Comment;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;

@Schema(description = "댓글 응답")
public record CommentResponse(
@Schema(description = "댓글 ID", example = "1")
Long id,
CommentMember member,
@Schema(description = "댓글 내용", example = "재밌어요!")
String content,
@Schema(description = "댓글 작성시각", example = "2023-08-01 10:56")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
LocalDateTime createdAt,
@Schema(description = "댓글 수정시각", example = "2023-08-01 13:56")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
LocalDateTime updatedAt
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3
혹시 어노테이션이 붙은 필드들은 위아래로 개행을 하나씩 넣어주는 건 어떤가요? 제가 지금 그렇게 하고 있어서요.
그게 좀 더 가독성이 있어보여서 그렇게 했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

이것도 record 컨벤션에 대해서 한번 이야기해봐야겠네요!

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 static CommentResponse from(final Comment comment) {
return new CommentResponse(
comment.getId(),
CommentMember.from(comment.getMember()),
comment.getContent(),
comment.getCreatedAt(),
comment.getUpdatedAt()
);
}

@Schema(description = "댓글 작성자 회원 응답")
record CommentMember(
@Schema(description = "댓글 작성자 회원 ID", example = "1")
Long id,
@Schema(description = "댓글 작성자 회원 닉네임", example = "votogether")
String nickname
) {

public static CommentMember from(final Member member) {
return new CommentMember(member.getId(), member.getNickname());
}

}
Comment on lines +38 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q
파일을 따로 안빼고 내부 레코드로 만든 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

member 객체로 묶기 위해 내부에서만 사용되는 클래스라 판단하였습니다!


}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static PostResponse of(final Post post, final Member loginMember) {

return new PostResponse(
post.getId(),
WriterResponse.of(writer.getId(), writer.getNickname().getValue()),
WriterResponse.of(writer.getId(), writer.getNickname()),
postBody.getTitle(),
postBody.getContent(),
getCategories(post),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package com.votogether.domain.post.repository;

import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.entity.comment.Comment;
import java.util.List;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Long> {

@EntityGraph(attributePaths = {"member"})
List<Comment> findAllByPostOrderByCreatedAtAsc(final Post post);
Comment on lines +11 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q
댓글을 가져올 땐 어차피 무조건 Member도 사용하게 되니까 @entitygraph를 설정하신건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

맞습니다! @EntityGraph를 통해 fetch join으로 회원도 항상 같이 가져올 수 있도록 하였습니다 :)


}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.dto.request.CommentUpdateRequest;
import com.votogether.domain.post.dto.response.CommentResponse;
import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.domain.post.exception.CommentExceptionType;
import com.votogether.domain.post.exception.PostExceptionType;
import com.votogether.domain.post.repository.CommentRepository;
import com.votogether.domain.post.repository.PostRepository;
import com.votogether.exception.NotFoundException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -38,6 +40,17 @@ public void createComment(
post.addComment(comment);
}

@Transactional(readOnly = true)
public List<CommentResponse> getComments(final Long postId) {
final Post post = postRepository.findById(postId)
.orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND));

return commentRepository.findAllByPostOrderByCreatedAtAsc(post)
.stream()
.map(CommentResponse::from)
.toList();
}

@Transactional
public void updateComment(
final Long postId,
Expand Down Expand Up @@ -68,4 +81,5 @@ public void deleteComment(final Long postId, final Long commentId, final Member

commentRepository.delete(comment);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void ExistByNickname() {
Member savedMember = memberRepository.save(MemberFixtures.MALE_20.get());

// when
boolean isExist = memberRepository.existsByNickname(savedMember.getNickname());
boolean isExist = memberRepository.existsByNickname(new Nickname(savedMember.getNickname()));

// then
assertThat(isExist).isTrue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void changeNickname() {
memberService.changeNickname(member, newNickname);

// then
assertThat(member.getNickname().getValue()).isEqualTo(newNickname);
assertThat(member.getNickname()).isEqualTo(newNickname);
}

@ParameterizedTest
Expand Down Expand Up @@ -89,7 +89,7 @@ void changeNicknameEqualToPrevious() {
Member member2 = memberRepository.save(MemberFixtures.MALE_30.get());

// when, then
assertThatThrownBy(() -> memberService.changeNickname(member1, member2.getNickname().getValue()))
assertThatThrownBy(() -> memberService.changeNickname(member1, member2.getNickname()))
.isInstanceOf(BadRequestException.class)
.hasMessage("이미 중복된 닉네임이 존재합니다.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.votogether.domain.post.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
Expand All @@ -12,11 +13,16 @@
import com.votogether.domain.member.service.MemberService;
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.dto.request.CommentUpdateRequest;
import com.votogether.domain.post.dto.response.CommentResponse;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.domain.post.service.PostCommentService;
import com.votogether.fixtures.MemberFixtures;
import com.votogether.global.jwt.TokenPayload;
import com.votogether.global.jwt.TokenProcessor;
import io.restassured.common.mapper.TypeRef;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand All @@ -29,6 +35,7 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.WebApplicationContext;

@WebMvcTest(PostCommentController.class)
Expand Down Expand Up @@ -122,6 +129,78 @@ void createComment() throws Exception {

}

@Nested
@DisplayName("게시글 댓글 목록 조회")
class GetComments {

@ParameterizedTest
@ValueSource(strings = {"@", "a", "가"})
@DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.")
void invalidPostIDType(String postId) throws Exception {
// given
TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L);
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());

// when, then
RestAssuredMockMvc.given().log().all()
.headers(HttpHeaders.AUTHORIZATION, "Bearer token")
.when().get("/posts/{postId}/comments", postId)
.then().log().all()
.status(HttpStatus.BAD_REQUEST)
.body("code", equalTo(-9998))
.body("message", containsString("postId는 Long 타입이 필요합니다."));
}

@Test
@DisplayName("정상적인 요청이라면 게시글 댓글 목록을 조회한다.")
void getComments() throws Exception {
// given
TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L);
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());
Comment on lines +166 to +169
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q
댓글 조회를 할 때 회원에 대한 인증이 필요한가요?
어느 누구든 어떤 게시글에 대한 댓글조회가 가능하지 않나요?
만약 그렇다면 filter에 대한 설정이 필요할 것 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

비회원 게시글 조회, 댓글 조회가 가능한 것을 알아버렸습니다 :) 수정완료했습니다 감사합니다 🙇🏻‍♂️


Member memberA = MemberFixtures.MALE_20.get();
Member memberB = MemberFixtures.FEMALE_20.get();
ReflectionTestUtils.setField(memberA, "id", 1L);
ReflectionTestUtils.setField(memberB, "id", 2L);

Comment commentA = Comment.builder()
.member(memberA)
.content("commentA")
.build();
Comment commentB = Comment.builder()
.member(memberB)
.content("commentA")
.build();
LocalDateTime now = LocalDateTime.now();
ReflectionTestUtils.setField(commentA, "createdAt", now);
ReflectionTestUtils.setField(commentB, "createdAt", now);

CommentResponse commentResponseA = CommentResponse.from(commentA);
CommentResponse commentResponseB = CommentResponse.from(commentB);
given(postCommentService.getComments(anyLong())).willReturn(List.of(commentResponseA, commentResponseB));

// when
List<CommentResponse> response = RestAssuredMockMvc.given().log().all()
.headers(HttpHeaders.AUTHORIZATION, "Bearer token")
.when().get("/posts/{postId}/comments", 1L)
.then().log().all()
.status(HttpStatus.OK)
.extract()
.as(new TypeRef<List<CommentResponse>>() {
});

// then
assertThat(response).usingRecursiveComparison()
.ignoringFieldsOfTypes(LocalDateTime.class)
.isEqualTo(List.of(commentResponseA, commentResponseB));
}

}

@Nested
@DisplayName("게시글 댓글 수정")
class UpdateComment {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.votogether.domain.post.repository;

import static org.assertj.core.api.Assertions.assertThat;

import com.votogether.RepositoryTest;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.repository.MemberRepository;
import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.entity.PostBody;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.fixtures.MemberFixtures;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

@RepositoryTest
class CommentRepositoryTest {

@Autowired
CommentRepository commentRepository;

@Autowired
MemberRepository memberRepository;

@Autowired
PostRepository postRepository;

@Test
@DisplayName("게시글의 댓글 목록을 조회한다.")
void findAllByPost() {
// given
Member member = memberRepository.save(MemberFixtures.MALE_20.get());
Post post = postRepository.save(
Post.builder()
.writer(member)
.postBody(PostBody.builder().title("titleA").content("contentA").build())
.deadline(LocalDateTime.of(2100, 7, 12, 0, 0))
.build()
);
Comment commentA = commentRepository.save(
Comment.builder()
.member(member)
.post(post)
.content("commentA")
.build()
);
Comment commentB = commentRepository.save(
Comment.builder()
.member(member)
.post(post)
.content("commentB")
.build()
);

// when
List<Comment> result = commentRepository.findAllByPostOrderByCreatedAtAsc(post);

// then
assertThat(result).containsExactly(commentA, commentB);
}

}
Loading