diff --git a/backend/src/main/java/shook/shook/part/application/PartCommentService.java b/backend/src/main/java/shook/shook/part/application/PartCommentService.java new file mode 100644 index 000000000..b8d801e92 --- /dev/null +++ b/backend/src/main/java/shook/shook/part/application/PartCommentService.java @@ -0,0 +1,39 @@ +package shook.shook.part.application; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shook.shook.part.application.dto.PartCommentRegisterRequest; +import shook.shook.part.application.dto.PartCommentResponse; +import shook.shook.part.domain.Part; +import shook.shook.part.domain.PartComment; +import shook.shook.part.domain.repository.PartCommentRepository; +import shook.shook.part.domain.repository.PartRepository; +import shook.shook.part.exception.PartException; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class PartCommentService { + + private final PartRepository partRepository; + private final PartCommentRepository partCommentRepository; + + @Transactional + public void register(final Long partId, final PartCommentRegisterRequest request) { + final Part part = partRepository.findById(partId) + .orElseThrow(PartException.PartNotExistException::new); + final PartComment partComment = PartComment.forSave(part, request.getContent()); + + part.addComment(partComment); + partCommentRepository.save(partComment); + } + + public List findPartReplies(final Long partId) { + final Part part = partRepository.findById(partId) + .orElseThrow(PartException.PartNotExistException::new); + + return PartCommentResponse.getList(part.getCommentsInRecentOrder()); + } +} diff --git a/backend/src/main/java/shook/shook/part/application/dto/PartCommentRegisterRequest.java b/backend/src/main/java/shook/shook/part/application/dto/PartCommentRegisterRequest.java new file mode 100644 index 000000000..8d9572ff5 --- /dev/null +++ b/backend/src/main/java/shook/shook/part/application/dto/PartCommentRegisterRequest.java @@ -0,0 +1,14 @@ +package shook.shook.part.application.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@Getter +public class PartCommentRegisterRequest { + + private String content; +} diff --git a/backend/src/main/java/shook/shook/part/application/dto/PartCommentResponse.java b/backend/src/main/java/shook/shook/part/application/dto/PartCommentResponse.java new file mode 100644 index 000000000..e1ba91bc5 --- /dev/null +++ b/backend/src/main/java/shook/shook/part/application/dto/PartCommentResponse.java @@ -0,0 +1,31 @@ +package shook.shook.part.application.dto; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import shook.shook.part.domain.PartComment; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class PartCommentResponse { + + private final Long id; + private final String content; + private final LocalDateTime createdAt; + + public static PartCommentResponse from(final PartComment partComment) { + return new PartCommentResponse( + partComment.getId(), + partComment.getContent(), + partComment.getCreatedAt() + ); + } + + public static List getList(final List partComments) { + return partComments.stream() + .map(PartCommentResponse::from) + .toList(); + } +} diff --git a/backend/src/main/java/shook/shook/part/domain/Part.java b/backend/src/main/java/shook/shook/part/domain/Part.java index bf880e7f8..6e31094ba 100644 --- a/backend/src/main/java/shook/shook/part/domain/Part.java +++ b/backend/src/main/java/shook/shook/part/domain/Part.java @@ -1,6 +1,7 @@ package shook.shook.part.domain; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -22,6 +23,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.part.exception.PartCommentException; import shook.shook.part.exception.PartException; import shook.shook.part.exception.VoteException; import shook.shook.song.domain.Song; @@ -54,6 +56,9 @@ public class Part { @OneToMany(mappedBy = "part") private final List votes = new ArrayList<>(); + @Embedded + private final PartComments comments = new PartComments(); + @Column(nullable = false, updatable = false) private LocalDateTime createdAt = LocalDateTime.now(); @@ -118,6 +123,13 @@ private void validateVote(final Vote vote) { } } + public void addComment(final PartComment comment) { + if (comment.isBelongToOtherPart(this)) { + throw new PartCommentException.CommentForOtherPartException(); + } + comments.addComment(comment); + } + public boolean hasEqualStartAndLength(final Part other) { return this.startSecond == other.startSecond && this.length.equals(other.length); } @@ -143,6 +155,14 @@ public int getVoteCount() { return votes.size(); } + public List getComments() { + return comments.getComments(); + } + + public List getCommentsInRecentOrder() { + return comments.getCommentsInRecentOrder(); + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/src/main/java/shook/shook/part/domain/PartComment.java b/backend/src/main/java/shook/shook/part/domain/PartComment.java new file mode 100644 index 000000000..13a269da5 --- /dev/null +++ b/backend/src/main/java/shook/shook/part/domain/PartComment.java @@ -0,0 +1,89 @@ +package shook.shook.part.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "part_comment") +@Entity +public class PartComment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private PartCommentContent content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "part_id", foreignKey = @ForeignKey(name = "none"), nullable = false, updatable = false) + @Getter(AccessLevel.NONE) + private Part part; + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + @PrePersist + private void prePersist() { + createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS); + } + + private PartComment(final Long id, final Part part, final String content) { + this.id = id; + this.part = part; + this.content = new PartCommentContent(content); + } + + public static PartComment saved(final Long id, final Part part, final String content) { + return new PartComment(id, part, content); + } + + public static PartComment forSave(final Part part, final String content) { + return new PartComment(null, part, content); + } + + public boolean isBelongToOtherPart(final Part part) { + return !this.part.equals(part); + } + + public String getContent() { + return content.getValue(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PartComment partComment = (PartComment) o; + if (Objects.isNull(partComment.id) || Objects.isNull(this.id)) { + return false; + } + return Objects.equals(id, partComment.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/src/main/java/shook/shook/part/domain/PartCommentContent.java b/backend/src/main/java/shook/shook/part/domain/PartCommentContent.java new file mode 100644 index 000000000..ccde285ec --- /dev/null +++ b/backend/src/main/java/shook/shook/part/domain/PartCommentContent.java @@ -0,0 +1,34 @@ +package shook.shook.part.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import shook.shook.part.exception.PartCommentException; +import shook.shook.util.StringChecker; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Embeddable +public class PartCommentContent { + + private static final int MAXIMUM_LENGTH = 200; + + @Column(name = "content", length = 200, nullable = false) + private String value; + + public PartCommentContent(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (StringChecker.isNullOrBlank(value)) { + throw new PartCommentException.NullOrEmptyPartCommentException(); + } + if (value.length() > MAXIMUM_LENGTH) { + throw new PartCommentException.TooLongPartCommentException(); + } + } +} diff --git a/backend/src/main/java/shook/shook/part/domain/PartComments.java b/backend/src/main/java/shook/shook/part/domain/PartComments.java new file mode 100644 index 000000000..c5ad458a7 --- /dev/null +++ b/backend/src/main/java/shook/shook/part/domain/PartComments.java @@ -0,0 +1,41 @@ +package shook.shook.part.domain; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import shook.shook.part.exception.PartCommentException; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Embeddable +public class PartComments { + + @OneToMany(mappedBy = "part") + private final List comments = new ArrayList<>(); + + public void addComment(final PartComment comment) { + validateComment(comment); + comments.add(comment); + } + + private void validateComment(final PartComment comment) { + if (comments.contains(comment)) { + throw new PartCommentException.DuplicateCommentExistException(); + } + } + + public List getComments() { + return new ArrayList<>(comments); + } + + public List getCommentsInRecentOrder() { + return comments.stream() + .sorted(Comparator.comparing(PartComment::getCreatedAt).reversed()) + .toList(); + } +} diff --git a/backend/src/main/java/shook/shook/part/domain/repository/PartCommentRepository.java b/backend/src/main/java/shook/shook/part/domain/repository/PartCommentRepository.java new file mode 100644 index 000000000..e2b139513 --- /dev/null +++ b/backend/src/main/java/shook/shook/part/domain/repository/PartCommentRepository.java @@ -0,0 +1,10 @@ +package shook.shook.part.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import shook.shook.part.domain.PartComment; + +@Repository +public interface PartCommentRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/shook/shook/part/exception/PartCommentException.java b/backend/src/main/java/shook/shook/part/exception/PartCommentException.java new file mode 100644 index 000000000..01cdd745d --- /dev/null +++ b/backend/src/main/java/shook/shook/part/exception/PartCommentException.java @@ -0,0 +1,32 @@ +package shook.shook.part.exception; + +public class PartCommentException extends RuntimeException { + + public static class NullOrEmptyPartCommentException extends PartCommentException { + + public NullOrEmptyPartCommentException() { + super(); + } + } + + public static class TooLongPartCommentException extends PartCommentException { + + public TooLongPartCommentException() { + super(); + } + } + + public static class CommentForOtherPartException extends PartCommentException { + + public CommentForOtherPartException() { + super(); + } + } + + public static class DuplicateCommentExistException extends PartCommentException { + + public DuplicateCommentExistException() { + super(); + } + } +} diff --git a/backend/src/main/java/shook/shook/part/ui/PartCommentController.java b/backend/src/main/java/shook/shook/part/ui/PartCommentController.java new file mode 100644 index 000000000..1c3ae472f --- /dev/null +++ b/backend/src/main/java/shook/shook/part/ui/PartCommentController.java @@ -0,0 +1,40 @@ +package shook.shook.part.ui; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shook.shook.part.application.PartCommentService; +import shook.shook.part.application.dto.PartCommentRegisterRequest; +import shook.shook.part.application.dto.PartCommentResponse; + +@RequiredArgsConstructor +@RequestMapping("/songs/{song_id}/parts/{part_id}/comments") +@RestController +public class PartCommentController { + + private final PartCommentService partCommentService; + + @PostMapping + public ResponseEntity registerPartComment( + @PathVariable(name = "part_id") final Long partId, + @RequestBody final PartCommentRegisterRequest request + ) { + partCommentService.register(partId, request); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping + public ResponseEntity> findPartReplies( + @PathVariable(name = "part_id") final Long partId + ) { + return ResponseEntity.ok(partCommentService.findPartReplies(partId)); + } +} diff --git a/backend/src/main/java/shook/shook/song/ui/HighVotedSongController.java b/backend/src/main/java/shook/shook/song/ui/HighVotedSongController.java index 9e692d93b..de6f1c434 100644 --- a/backend/src/main/java/shook/shook/song/ui/HighVotedSongController.java +++ b/backend/src/main/java/shook/shook/song/ui/HighVotedSongController.java @@ -20,6 +20,6 @@ public class HighVotedSongController { public ResponseEntity> showHighVotedSongs() { final List response = songService.findHighVotedSongs(); - return ResponseEntity.ok().body(response); + return ResponseEntity.ok(response); } } diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index a3dc0f559..eb5cef42f 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -24,4 +24,13 @@ create table if not exists vote part_id bigint, created_at timestamp(6) not null, primary key (id) +); + +create table if not exists part_comment +( + id bigint auto_increment, + part_id bigint not null, + content varchar(200) not null, + created_at timestamp(6) not null, + primary key (id) ) diff --git a/backend/src/test/java/shook/shook/part/application/PartCommentServiceTest.java b/backend/src/test/java/shook/shook/part/application/PartCommentServiceTest.java new file mode 100644 index 000000000..bf30bb9a0 --- /dev/null +++ b/backend/src/test/java/shook/shook/part/application/PartCommentServiceTest.java @@ -0,0 +1,74 @@ +package shook.shook.part.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import shook.shook.part.application.dto.PartCommentRegisterRequest; +import shook.shook.part.application.dto.PartCommentResponse; +import shook.shook.part.domain.Part; +import shook.shook.part.domain.PartComment; +import shook.shook.part.domain.PartLength; +import shook.shook.part.domain.repository.PartCommentRepository; +import shook.shook.part.domain.repository.PartRepository; +import shook.shook.song.domain.Song; +import shook.shook.song.domain.repository.SongRepository; +import shook.shook.support.UsingJpaTest; + +class PartCommentServiceTest extends UsingJpaTest { + + private static Part SAVED_PART; + + @Autowired + private PartRepository partRepository; + + @Autowired + private SongRepository songRepository; + + @Autowired + private PartCommentRepository partCommentRepository; + + private PartCommentService partCommentService; + + @BeforeEach + void setUp() { + final Song savedSong = songRepository.save(new Song("제목", "비디오URL", "이미지URL", "가수", 30)); + SAVED_PART = partRepository.save(Part.forSave(3, PartLength.SHORT, savedSong)); + partCommentService = new PartCommentService(partRepository, partCommentRepository); + } + + @DisplayName("파트의 댓글을 등록한다.") + @Test + void register() { + //given + final PartCommentRegisterRequest request = new PartCommentRegisterRequest("댓글 내용"); + + //when + partCommentService.register(SAVED_PART.getId(), request); + saveAndClearEntityManager(); + + //then + final Part part = partRepository.findById(SAVED_PART.getId()).get(); + assertThat(part.getComments()).hasSize(1); + assertThat(part.getComments().get(0).getContent()).isEqualTo("댓글 내용"); + } + + @DisplayName("파트의 모든 댓글을 조회하여 반환한다.") + @Test + void findPartReplies() { + //given + final PartComment early = partCommentRepository.save(PartComment.forSave(SAVED_PART, "1")); + final PartComment late = partCommentRepository.save(PartComment.forSave(SAVED_PART, "2")); + + //when + saveAndClearEntityManager(); + final List partReplies = partCommentService.findPartReplies( + SAVED_PART.getId()); + + //then + assertThat(partReplies).usingRecursiveComparison().isEqualTo(List.of(late, early)); + } +} diff --git a/backend/src/test/java/shook/shook/part/domain/PartCommentContentTest.java b/backend/src/test/java/shook/shook/part/domain/PartCommentContentTest.java new file mode 100644 index 000000000..9f1deaeb6 --- /dev/null +++ b/backend/src/test/java/shook/shook/part/domain/PartCommentContentTest.java @@ -0,0 +1,47 @@ +package shook.shook.part.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import shook.shook.part.exception.PartCommentException; + +class PartCommentContentTest { + + @DisplayName("댓글의 내용을 뜻하는 객체를 생성한다.") + @Test + void create_success() { + //given + //when + //then + assertDoesNotThrow(() -> new PartCommentContent("댓글 내용")); + } + + @DisplayName("댓글의 내용이 유효하지 않으면 예외를 던진다.") + @NullSource + @ParameterizedTest(name = "댓글의 내용이 \"{0}\" 일 때") + @ValueSource(strings = {"", " "}) + void create_fail_nullOrEmpty(final String content) { + //given + //when + //then + assertThatThrownBy(() -> new PartCommentContent(content)) + .isInstanceOf(PartCommentException.NullOrEmptyPartCommentException.class); + } + + @DisplayName("댓글의 내용의 길이가 200를 넘을 경우 예외를 던진다.") + @Test + void create_fail_lengthOver200() { + //given + final String content = ".".repeat(201); + + //when + //then + assertThatThrownBy(() -> new PartCommentContent(content)) + .isInstanceOf(PartCommentException.TooLongPartCommentException.class); + } +} diff --git a/backend/src/test/java/shook/shook/part/domain/PartCommentTest.java b/backend/src/test/java/shook/shook/part/domain/PartCommentTest.java new file mode 100644 index 000000000..d2dbc6505 --- /dev/null +++ b/backend/src/test/java/shook/shook/part/domain/PartCommentTest.java @@ -0,0 +1,72 @@ +package shook.shook.part.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import shook.shook.song.domain.Song; + +class PartCommentTest { + + private final Song song = new Song("제목", "비디오URL", "이미지URL", "가수", 30); + private final Part firstPart = Part.saved(1L, 4, PartLength.SHORT, song); + private final Part secondPart = Part.saved(2L, 4, PartLength.SHORT, song); + + @DisplayName("새로운 댓글을 생성한다.") + @Test + void create_new() { + //given + //when + //then + assertDoesNotThrow(() -> PartComment.forSave(firstPart, "댓글 내용")); + } + + @DisplayName("이미 작성된 댓글을 생성한다.") + @Test + void create_exist() { + //given + //when + //then + assertDoesNotThrow(() -> PartComment.saved(1L, firstPart, "댓글 내용")); + } + + @DisplayName("댓글의 내용을 반환한다.") + @Test + void getContent() { + //given + final PartComment partComment = PartComment.forSave(firstPart, "댓글 내용"); + + //when + final String content = partComment.getContent(); + + //then + assertThat(content).isEqualTo("댓글 내용"); + } + + @DisplayName("댓글이 다른 파트에 포함 되어있는지 여부를 반환한다. ( 같은 파트일 때 false 를 반환한다. )") + @Test + void isBelongToOtherPart_samePart() { + //given + final PartComment partComment = PartComment.forSave(firstPart, "댓글 내용"); + + //when + final boolean isBelongTo = partComment.isBelongToOtherPart(firstPart); + + //then + assertThat(isBelongTo).isFalse(); + } + + @DisplayName("댓글이 다른 파트에 포함 되어있는지 여부를 반환한다. ( 다른 파트일 때 true 를 반환한다. )") + @Test + void isBelongToOtherPart_otherPart() { + //given + final PartComment partComment = PartComment.forSave(firstPart, "댓글 내용"); + + //when + final boolean isBelongTo = partComment.isBelongToOtherPart(secondPart); + + //then + assertThat(isBelongTo).isTrue(); + } +} diff --git a/backend/src/test/java/shook/shook/part/domain/PartCommentsTest.java b/backend/src/test/java/shook/shook/part/domain/PartCommentsTest.java new file mode 100644 index 000000000..00f676996 --- /dev/null +++ b/backend/src/test/java/shook/shook/part/domain/PartCommentsTest.java @@ -0,0 +1,66 @@ +package shook.shook.part.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import shook.shook.part.exception.PartCommentException; +import shook.shook.song.domain.Song; + +class PartCommentsTest { + + @DisplayName("파트에 댓글을 성공적으로 추가한 경우") + @Test + void addComment_success() { + //given + final Song song = new Song("제목", "비디오URL", "이미지URL", "가수", 30); + final Part part = Part.saved(1L, 5, PartLength.SHORT, song); + + final PartComments comments = new PartComments(); + + //when + comments.addComment(PartComment.saved(1L, part, "댓글 내용")); + + //then + assertThat(comments.getComments()).hasSize(1); + } + + @DisplayName("파트에 댓글이 이미 존재하는 댓글인 경우") + @Test + void addComment_exist() { + //given + final Song song = new Song("제목", "비디오URL", "이미지URL", "가수", 30); + final Part part = Part.saved(1L, 5, PartLength.SHORT, song); + final PartComments partComments = new PartComments(); + + //when + partComments.addComment(PartComment.saved(1L, part, "댓글 내용")); + + //then + assertThatThrownBy(() -> partComments.addComment(PartComment.saved(1L, part, "댓글 내용"))) + .isInstanceOf(PartCommentException.DuplicateCommentExistException.class); + } + + @DisplayName("최신 순으로 정렬된 댓글을 반환한다.") + @Test + void getRepliesInRecentOrder() { + //given + final Song song = new Song("제목", "비디오URL", "이미지URL", "가수", 30); + final Part part = Part.saved(1L, 5, PartLength.SHORT, song); + final PartComments comments = new PartComments(); + + final PartComment early = PartComment.saved(1L, part, "댓글입니다."); + final PartComment late = PartComment.saved(2L, part, "댓글이였습니다."); + + comments.addComment(late); + comments.addComment(early); + + //when + final List repliesInRecentOrder = comments.getCommentsInRecentOrder(); + + //then + assertThat(repliesInRecentOrder).usingRecursiveComparison().isEqualTo(List.of(late, early)); + } +} diff --git a/backend/src/test/java/shook/shook/part/domain/PartTest.java b/backend/src/test/java/shook/shook/part/domain/PartTest.java index bd7f84057..1230d7c92 100644 --- a/backend/src/test/java/shook/shook/part/domain/PartTest.java +++ b/backend/src/test/java/shook/shook/part/domain/PartTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import shook.shook.part.exception.PartCommentException; import shook.shook.part.exception.PartException; import shook.shook.part.exception.VoteException; import shook.shook.song.domain.Song; @@ -208,4 +209,38 @@ void exist() { final String playDuration = String.format("?start=%d&end=%d", startSecond, endSecond); assertThat(startAndEndUrlPathParameter).isEqualTo(playDuration); } + + @DisplayName("파트에 댓글을 추가한다.") + @Nested + class AddComment { + + @DisplayName("성공적으로 추가한 경우") + @Test + void success() { + //given + final Song song = new Song("제목", "비디오URL", "이미지URL", "가수", 30); + final Part part = Part.saved(1L, 5, PartLength.SHORT, song); + + //when + part.addComment(PartComment.saved(1L, part, "댓글 내용")); + + //then + assertThat(part.getComments()).hasSize(1); + } + + @DisplayName("다른 파트의 댓글을 추가한 경우") + @Test + void belongToOtherPart() { + //given + final Song song = new Song("제목", "비디오URL", "이미지URL", "가수", 30); + final Part firstPart = Part.saved(1L, 5, PartLength.SHORT, song); + final Part secondPart = Part.saved(2L, 5, PartLength.SHORT, song); + + //when + //then + assertThatThrownBy( + () -> firstPart.addComment(PartComment.saved(2L, secondPart, "댓글 내용"))) + .isInstanceOf(PartCommentException.CommentForOtherPartException.class); + } + } } diff --git a/backend/src/test/java/shook/shook/part/domain/repository/PartCommentRepositoryTest.java b/backend/src/test/java/shook/shook/part/domain/repository/PartCommentRepositoryTest.java new file mode 100644 index 000000000..6e39d47e0 --- /dev/null +++ b/backend/src/test/java/shook/shook/part/domain/repository/PartCommentRepositoryTest.java @@ -0,0 +1,66 @@ +package shook.shook.part.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import shook.shook.part.domain.Part; +import shook.shook.part.domain.PartComment; +import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Song; +import shook.shook.song.domain.repository.SongRepository; +import shook.shook.support.UsingJpaTest; + +class PartCommentRepositoryTest extends UsingJpaTest { + + private static Part SAVED_PART; + + @Autowired + private PartRepository partRepository; + + @Autowired + private SongRepository songRepository; + + @Autowired + private PartCommentRepository partCommentRepository; + + @BeforeEach + void setUp() { + Song SAVED_SONG = songRepository.save(new Song("제목", "비디오URL", "이미지URL", "가수", 30)); + SAVED_PART = partRepository.save(Part.forSave(3, PartLength.SHORT, SAVED_SONG)); + } + + @DisplayName("PartReply 를 저장한다.") + @Test + void save() { + //given + final PartComment partComment = PartComment.forSave(SAVED_PART, "댓글 내용"); + + //when + final PartComment savedPartComment = partCommentRepository.save(partComment); + + //then + assertThat(savedPartComment).isSameAs(partComment); + assertThat(partComment.getId()).isNotNull(); + } + + @DisplayName("Part 을 저장할 때의 시간 정보로 createAt이 자동 생성된다.") + @Test + void createdAt() { + //given + final PartComment partComment = PartComment.forSave(SAVED_PART, "댓글 내용"); + + //when + final LocalDateTime prev = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS); + final PartComment savedPartComment = partCommentRepository.save(partComment); + final LocalDateTime after = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS); + + //then + assertThat(savedPartComment).isSameAs(partComment); + assertThat(partComment.getCreatedAt()).isBetween(prev, after); + } +} diff --git a/backend/src/test/java/shook/shook/part/ui/PartCommentControllerTest.java b/backend/src/test/java/shook/shook/part/ui/PartCommentControllerTest.java new file mode 100644 index 000000000..4588712d1 --- /dev/null +++ b/backend/src/test/java/shook/shook/part/ui/PartCommentControllerTest.java @@ -0,0 +1,88 @@ +package shook.shook.part.ui; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import shook.shook.part.application.dto.PartCommentRegisterRequest; +import shook.shook.part.application.dto.PartCommentResponse; +import shook.shook.part.domain.Part; +import shook.shook.part.domain.PartComment; +import shook.shook.part.domain.PartLength; +import shook.shook.part.domain.repository.PartCommentRepository; +import shook.shook.part.domain.repository.PartRepository; +import shook.shook.song.domain.Song; +import shook.shook.song.domain.repository.SongRepository; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class PartCommentControllerTest { + + @LocalServerPort + public int port; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @Autowired + private SongRepository songRepository; + + @Autowired + private PartRepository partRepository; + + @Autowired + private PartCommentRepository partCommentRepository; + + @DisplayName("파트에 댓글 등록시 상태코드 201를 반환한다.") + @Test + void registerPartReply() { + //given + final Song song = songRepository.save(new Song("제목", "비디오URL", "이미지URL", "가수", 30)); + final Part part = partRepository.save(Part.forSave(1, PartLength.SHORT, song)); + final PartCommentRegisterRequest request = new PartCommentRegisterRequest("댓글 내용"); + + //when + //then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .post("/songs/" + song.getId() + "/parts/" + part.getId() + "/comments") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()); + } + + @DisplayName("파트의 모든 댓글을 조회하여 상태코드 200과 함께 응답한다.") + @Test + void findPartReplies() { + //given + final Song song = songRepository.save(new Song("제목", "비디오URL", "이미지URL", "가수", 30)); + final Part part = partRepository.save(Part.forSave(1, PartLength.SHORT, song)); + partCommentRepository.save(PartComment.forSave(part, "댓글 내용")); + + //when + final List response = RestAssured.given().log().all() + .when().log().all() + .get("/songs/" + song.getId() + "/parts/" + part.getId() + "/comments") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .body() + .jsonPath() + .getList(".", PartCommentResponse.class); + + //then + assertThat(response).hasSize(1); + assertThat(response.get(0).getContent()).isEqualTo("댓글 내용"); + } +}