728x90
320x100
🎯 오늘의 목표
- 댓글(Comment) 도메인 개발
⚙️ 진행한 작업
- 댓글(Comment) 엔티티 생성
- 댓글(Comment) 레파지토리 생성
- 댓글(Comment) 서비스 생성
- 댓글(Comment) 관련 예외 생성
- 댓글(Comment) 도메인 단위테스트
🛠️ 개발내용
📌 댓글(Comment) 엔티티 생성
package ddururi.bookbookclub.domain.comment.entity;
import java.time.LocalDateTime;
/**
* 댓글(Comment) 엔티티
* - 사용자가 특정 피드에 작성한 댓글을 저장
* - 피드(Feed)와 댓글(Comment)은 다대일 관계
* - 대댓글은 지원하지 않음
*/
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 댓글 내용 */
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
/** 댓글 작성자 */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
/** 댓글이 작성된 피드 */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "feed_id", nullable = false)
private Feed feed;
/** 댓글 좋아요 수 */
@Column(nullable = false)
private int likeCount = 0;
/** 댓글 생성일 */
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
/** 댓글 수정일 */
@LastModifiedDate
private LocalDateTime updatedAt;
/**
* 댓글 생성 정적 메서드
*/
public static Comment create(String content, User user, Feed feed) {
Comment comment = new Comment();
comment.content = content;
comment.user = user;
comment.feed = feed;
return comment;
}
/**
* 댓글 내용 수정
*/
public void updateContent(String content) {
this.content = content;
}
}
📌 댓글(Comment) 레파지토리 생성
package ddururi.bookbookclub.domain.comment.repository;
import ddururi.bookbookclub.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* 댓글(Comment) 레포지토리
* - 댓글 CRUD 및 피드 ID 기준 댓글 조회 기능 제공
*/
public interface CommentRepository extends JpaRepository<Comment, Long> {
/**
* 특정 피드 ID에 작성된 댓글 목록 조회 (최신순 정렬)
* @param feedId 피드 ID
* @return 댓글 리스트
*/
List<Comment> findByFeedIdOrderByCreatedAtDesc(Long feedId);
}
📌 댓글(Comment) 서비스 생성
package ddururi.bookbookclub.domain.comment.service;
/**
* 댓글(Comment) 서비스
* - 댓글 등록, 조회, 삭제 기능 제공
*/
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {
private final CommentRepository commentRepository;
private final FeedRepository feedRepository;
/**
* 댓글 등록
* @param content 댓글 내용
* @param user 작성자
* @param feedId 댓글이 달릴 피드 ID
* @return 저장된 댓글
*/
public Comment createComment(String content, User user, Long feedId) {
Feed feed = feedRepository.findById(feedId)
.orElseThrow(FeedNotFoundException::new);
Comment comment = Comment.create(content, user, feed);
return commentRepository.save(comment);
}
/**
* 특정 피드에 작성된 댓글 목록 조회 (최신순)
* @param feedId 피드 ID
* @return 댓글 리스트
*/
public List<Comment> getCommentsByFeed(Long feedId) {
return commentRepository.findByFeedIdOrderByCreatedAtDesc(feedId);
}
/**
* 댓글 삭제
* @param commentId 댓글 ID
* @param user 삭제 요청 사용자
*/
public void deleteComment(Long commentId, User user) {
Comment comment = commentRepository.findById(commentId)
.orElseThrow(CommentNotFoundException::new);
if (!comment.getUser().getId().equals(user.getId())) {
throw new CommentAccessDeniedException();
}
commentRepository.delete(comment);
}
}
📌 댓글(Comment) 관련예외 생성
CommentAccessDeniedException.java
package ddururi.bookbookclub.domain.comment.exception;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;
/**
* 댓글 삭제 권한이 없는 경우 발생
*/
@Getter
public class CommentAccessDeniedException extends RuntimeException {
private final ErrorCode errorCode;
public CommentAccessDeniedException() {
super(ErrorCode.COMMENT_ACCESS_DENIED.getMessage());
this.errorCode = ErrorCode.COMMENT_ACCESS_DENIED;
}
}
CommentNotFoundException.java
package ddururi.bookbookclub.domain.comment.exception;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;
/**
* 존재하지 않는 댓글에 접근할 때 발생
*/
@Getter
public class CommentNotFoundException extends RuntimeException {
private final ErrorCode errorCode;
public CommentNotFoundException() {
super(ErrorCode.COMMENT_NOT_FOUND.getMessage());
this.errorCode = ErrorCode.COMMENT_NOT_FOUND;
}
}
📌 댓글(Comment) 도메인 단위테스트
package ddururi.bookbookclub.domain.comment.service;
import ddururi.bookbookclub.domain.comment.entity.Comment;
import ddururi.bookbookclub.domain.comment.exception.CommentAccessDeniedException;
import ddururi.bookbookclub.domain.comment.exception.CommentNotFoundException;
import ddururi.bookbookclub.domain.comment.repository.CommentRepository;
import ddururi.bookbookclub.domain.feed.entity.Feed;
import ddururi.bookbookclub.domain.feed.exception.FeedNotFoundException;
import ddururi.bookbookclub.domain.feed.repository.FeedRepository;
import ddururi.bookbookclub.domain.user.entity.User;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class CommentServiceTest {
@Mock
private CommentRepository commentRepository;
@Mock
private FeedRepository feedRepository;
@InjectMocks
private CommentService commentService;
public CommentServiceTest() {
MockitoAnnotations.openMocks(this);
}
@Nested
@DisplayName("댓글 등록 테스트")
class CreateCommentTest {
@Test
@DisplayName("댓글 등록 성공")
void createComment_success() {
// given
User user = User.create("test@email.com", "encodedPassword", "nickname");
Feed feed = mock(Feed.class);
when(feedRepository.findById(1L)).thenReturn(Optional.of(feed));
when(commentRepository.save(any(Comment.class))).thenAnswer(invocation -> invocation.getArgument(0));
// when
Comment comment = commentService.createComment("테스트 댓글", user, 1L);
// then
assertThat(comment.getContent()).isEqualTo("테스트 댓글");
assertThat(comment.getUser()).isEqualTo(user);
assertThat(comment.getFeed()).isEqualTo(feed);
}
@Test
@DisplayName("댓글 등록 실패 - 피드 없음")
void createComment_feedNotFound() {
// given
User user = User.create("test@email.com", "encodedPassword", "nickname");
when(feedRepository.findById(1L)).thenReturn(Optional.empty());
// when & then
assertThatThrownBy(() -> commentService.createComment("테스트 댓글", user, 1L))
.isInstanceOf(FeedNotFoundException.class);
}
}
@Nested
@DisplayName("댓글 조회 테스트")
class GetCommentsByFeedTest {
@Test
@DisplayName("피드 ID로 댓글 조회 성공")
void getCommentsByFeed_success() {
// given
Feed feed = mock(Feed.class);
List<Comment> comments = List.of(
Comment.create("댓글1", User.create("user1@email.com", "pass", "nickname1"), feed),
Comment.create("댓글2", User.create("user2@email.com", "pass", "nickname2"), feed)
);
when(commentRepository.findByFeedIdOrderByCreatedAtDesc(1L)).thenReturn(comments);
// when
List<Comment> result = commentService.getCommentsByFeed(1L);
// then
assertThat(result).hasSize(2);
assertThat(result.get(0).getContent()).isEqualTo("댓글1");
assertThat(result.get(1).getContent()).isEqualTo("댓글2");
}
}
@Nested
@DisplayName("댓글 삭제 테스트")
class DeleteCommentTest {
@Test
@DisplayName("댓글 삭제 성공")
void deleteComment_success() {
// given
User user = User.create("test@email.com", "encodedPassword", "nickname");
ReflectionTestUtils.setField(user, "id", 1L);
Feed feed = mock(Feed.class);
Comment comment = Comment.create("삭제할 댓글", user, feed);
ReflectionTestUtils.setField(comment, "id", 1L);
when(commentRepository.findById(1L)).thenReturn(Optional.of(comment));
// when
commentService.deleteComment(1L, user);
// then
verify(commentRepository, times(1)).delete(comment);
}
@Test
@DisplayName("댓글 삭제 실패 - 댓글 없음")
void deleteComment_commentNotFound() {
// given
when(commentRepository.findById(1L)).thenReturn(Optional.empty());
// when & then
assertThatThrownBy(() -> commentService.deleteComment(1L,
User.create("test@email.com", "encodedPassword", "nickname")))
.isInstanceOf(CommentNotFoundException.class);
}
@Test
@DisplayName("댓글 삭제 실패 - 권한 없음")
void deleteComment_accessDenied() {
// given
User owner = User.create("owner@email.com", "encodedPassword", "ownerNick");
User otherUser = User.create("other@email.com", "encodedPassword", "otherNick");
ReflectionTestUtils.setField(owner, "id", 1L);
ReflectionTestUtils.setField(otherUser, "id", 2L);
Feed feed = mock(Feed.class);
Comment comment = Comment.create("권한 없는 삭제 시도", owner, feed);
when(commentRepository.findById(1L)).thenReturn(Optional.of(comment));
// when & then
assertThatThrownBy(() -> commentService.deleteComment(1L, otherUser))
.isInstanceOf(CommentAccessDeniedException.class);
}
}
}
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #024] JWT 토큰이 없는데도 200 OK 가 뜬다? (해결방법) (0) | 2025.04.30 |
---|---|
[개발일지 #023] 댓글(Comment) API 구현 및 테스트 (0) | 2025.04.30 |
[개발일지 #021] 좋아요(Like) 도메인 API 구현 및 테스트 (0) | 2025.04.30 |
[개발일지 #020] 좋아요(Like) 도메인 개발 및 단위테스트 (0) | 2025.04.29 |
[개발일지 #019] 피드(Feed) 도메인 API 구현 및 테스트 (0) | 2025.04.29 |