Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
242beaf
docs: TODO List 작성
Apr 1, 2025
f25168d
feat: 게시글 엔티티 생성
Apr 1, 2025
fd17a8e
feat: 게시글 생성 요청 DTO 구현
Apr 1, 2025
181336e
feat: 게시글 생성 repository 구현
Apr 1, 2025
c32824a
feat: 게시글 생성 service 구현
Apr 1, 2025
4397fb2
feat: 게시글 생성 facade 구현
Apr 1, 2025
52c57af
feat: 게시글 생성 API 구현
Apr 1, 2025
a22732b
feat: 게시글 응답 DTO 생성
Apr 1, 2025
67b764f
refactor: 게시글 생성 시 반환값을 PostResponseDto로 변경
Apr 1, 2025
94e7e44
test: 게시글 생성 API test 추가
Apr 1, 2025
46599db
feat: 생성/수정/삭제 시 해당 데이터의 변경 시점을 기록하는 필드 추가
Apr 1, 2025
e0bf58b
feat: 유효성 검사 추가
Apr 1, 2025
85ba6e5
refactor: 삭제 관련 로직 BaseEntity 클래스로 이동
Apr 1, 2025
b5422ba
refactor: dto 패키지 이동
Apr 2, 2025
9023c06
feat: 게시글 단건 조회 서비스 구현
Apr 2, 2025
4a50b41
feat: 게시글 단건 조회 facade 구현
Apr 2, 2025
5803869
feat: 게시글 단건 조회 API 생성
Apr 2, 2025
4afca5f
feat: 댓글 엔티티 생성
Apr 2, 2025
43eef1d
feat: 댓글 응답, 생성 요청 DTO 구현
Apr 2, 2025
5ac1353
feat: 댓글 생성 Repository 구현
Apr 2, 2025
a685619
feat: 댓글 생성 service 구현
Apr 2, 2025
fe21350
feat: 댓글 생성 facade 구현
Apr 2, 2025
c547de9
feat: 댓글 생성 API 구현
Apr 2, 2025
7562207
test: 댓글 생성, 게시글 단건 조회 테스트 추가
Apr 2, 2025
1bc8163
feat: 댓글 생성 요청 DTO 유효성 검사 추가
Apr 2, 2025
dbdaab3
docs: TODO List 수정
Apr 2, 2025
e23bd25
feat: 게시글 수정 요청 DTO 생성
Apr 2, 2025
4010b7d
feat: 게시글 수정 서비스 구현
Apr 2, 2025
cc549d5
feat: 게시글 수정 API 생성
Apr 2, 2025
4f98c7d
test: 게시글 수정 API 테스트 추가
Apr 2, 2025
f400464
feat: 게시글 삭제 서비스 구현
Apr 2, 2025
9140ebe
feat: 게시글 삭제 API 생성
Apr 2, 2025
e7683aa
test: 게시글 삭제 테스트 추가
Apr 2, 2025
f37d657
feat: 삭제된 게시글 조회/수정/삭제 제한
Apr 2, 2025
aa205a2
feat: 댓글 수정 요청 DTO 생성
Apr 2, 2025
2f8205d
refactor: 게시글 삭제 확인 기능 분리
Apr 2, 2025
23a85fd
feat: 댓글 아이디 기반 조회 repository 구현
Apr 2, 2025
5645fdf
feat: 댓글 수정 서비스 구현
Apr 2, 2025
94e233a
feat: 댓글 엔티티에 수정 메서드 추가
Apr 2, 2025
1c2c8cf
feat: 댓글 수정 API 생성
Apr 2, 2025
5a4bb9d
test: 댓글 수정 테스트 추가
Apr 2, 2025
fabe759
feat: 댓글 삭제 서비스 구현
Apr 2, 2025
f283982
feat: 댓글 삭제 API 생성
Apr 2, 2025
0ad9b06
test: 댓글 삭제 테스트 추가
Apr 2, 2025
7c0e566
feat: 게시글 응답 DTO에 댓글 리스트 추가
Apr 2, 2025
dcb69bf
feat: 게시글 조회 시 게시글에 포함된 댓글 목록 조회
Apr 2, 2025
faa5d60
feat: 게시글 삭제 시 게시글에 포함된 댓글 목록 삭제
Apr 2, 2025
e296b84
refactor: 조회 성능을 위해 트랜젝션 readOnly = true 적용
Apr 2, 2025
6a9ae49
feat: 정렬이 있는 DTO와 정렬이 없는 DTO 분리
Apr 2, 2025
bcd433b
feat: 댓글 정렬 기능 구현
Apr 2, 2025
89756b8
feat: 게시글 조회 시 삭제되지 않는 댓글만 조회 구현
Apr 2, 2025
68d4362
test: 게시글 테스트 수정
Apr 2, 2025
76e1762
refactor: 게시글과 댓글 패키지 분리
Apr 2, 2025
91df848
docs: TODO List 수정
Apr 2, 2025
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
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## TODO List

### POST
- [x] 게시글 작성
- [x] 게시글 제목, 내용은 필수 항목
- [x] 게시글 작성 일자 자동 기록
- 유효성 검사
- [x] 게시글 제목이 10자 미만 100자 초과일 경우
- [x] 게시글 내용이 10자 미만일 경우
- [x] 게시글 수정
- [x] 제목과 내용만 수정 가능
- [x] 게시글 수정 일자 자동 기록
- 예외 케이스
- [x] 존재하지 않는 게시글을 수정할 경우
- [x] 삭제된 게시글을 수정할 경우
- 유효성 검사
- [x] 수정 제목이 10자 미만 100자 초과일 경우
- [x] 수정 내용이 10자 미만일 경우
- [x] 게시글 삭제
- [x] 삭제 시 게시글과 함께 해당 게시글의 모든 댓글도 삭제 처리
- [x] 삭제 후 복구 불가능
- [x] 게시글 삭제 시 삭제 상태에 대한 변경만 처리 (soft-delete)
- [x] 게시글 삭제 일자 기록
- 예외 케이스
- [x] 게시글이 이미 삭제된 경우
- [x] 존재하지 않는 게시글을 삭제할 경우
- [x] 게시글 단건 조회
- [x] 게시글에 포함된 모든 댓글 목록을 조회
- [x] 삭제된 데이터는 조회 불가능
- [x] Pagination 시 요청 Page Size가 10/30/50이 아닌 경우 10으로 고정
- [x] 삭제된 댓글 조회 불가능
- 예외 케이스
- [x] 존재하지 않는 게시글일 경우

### COMMENT
- [x] 댓글 등록
- [x] 댓글 내용은 필수 항목
- [x] 댓글 등록 일자 자동 기록
- 예외 케이스
- [x] 게시글이 존재하지 않을 경우
- [x] 삭제된 게시글일 경우
- 유효성 검사
- [x] 댓글 내용이 10자 미만일 경우
- [x] 댓글 수정
- [x] 댓글 내용만 수정 가능
- [x] 댓글 수정 일자 자동 기록
- 예외 케이스
- [x] 삭제된 게시글일 경우
- [x] 삭제된 댓글일 경우
- 유효성 검사
- [x] 수정 내용이 10자 미만일 경우
- [x] 댓글 삭제
- [x] 삭제 후 복구 불가능
- [x] 댓글 삭제 시 삭제 상태에 대한 변경만 처리 (soft delete)
- 예외 케이스
- [x] 게시글이 존재하지 않을 경우
- [x] 삭제된 게시글일 경우
- [x] 댓글이 이미 삭제된 경우


## 구현 시 어려웠던 부분
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.sparta.board.comment.application.facade;

import io.sparta.board.comment.presentation.dto.request.CommentCreateRequestDto;
import io.sparta.board.comment.presentation.dto.request.CommentUpdateRequestDto;
import io.sparta.board.comment.presentation.dto.response.CommentResponseDto;
import java.util.UUID;

public interface CommentFacade {

CommentResponseDto createComment(CommentCreateRequestDto requestDto, UUID postId);

CommentResponseDto updateComment(UUID commentId, CommentUpdateRequestDto requestDto);

void deleteComment(UUID commentId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.sparta.board.comment.application.facade;

import io.sparta.board.comment.application.usecase.CommentService;
import io.sparta.board.comment.model.entity.Comment;
import io.sparta.board.comment.presentation.dto.request.CommentCreateRequestDto;
import io.sparta.board.comment.presentation.dto.request.CommentUpdateRequestDto;
import io.sparta.board.comment.presentation.dto.response.CommentResponseDto;
import io.sparta.board.post.application.usecase.PostService;
import io.sparta.board.post.model.entity.Post;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CommentFacadeImpl implements CommentFacade {

private final PostService postService;
private final CommentService commentService;

@Override
public CommentResponseDto createComment(CommentCreateRequestDto requestDto, UUID postId) {
Post post = postService.getPost(postId);
postService.isDeleted(post);
Comment comment = requestDto.createComment(post);
Comment savedComment = commentService.createComment(comment);
return CommentResponseDto.toResponseDto(savedComment);
}

@Override
public CommentResponseDto updateComment(UUID commentId, CommentUpdateRequestDto requestDto) {
Comment comment = commentService.getComment(commentId);
postService.isDeleted(comment.getPost());
commentService.isDeleted(comment);
commentService.updateComment(comment, requestDto);
return CommentResponseDto.toResponseDto(comment);
}

@Override
public void deleteComment(UUID commentId) {
Comment comment = commentService.getComment(commentId);
commentService.isDeleted(comment);
commentService.deleteComment(comment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.sparta.board.comment.application.usecase;

import io.sparta.board.comment.model.entity.Comment;
import io.sparta.board.comment.presentation.dto.request.CommentUpdateRequestDto;
import java.util.List;
import java.util.UUID;
import org.springframework.data.domain.Pageable;

public interface CommentService {

Comment createComment(Comment comment);

Comment getComment(UUID commentId);

void updateComment(Comment comment, CommentUpdateRequestDto requestDto);

void isDeleted(Comment comment);

void deleteComment(Comment comment);

List<Comment> getComments(UUID postId, Pageable pageable);

List<Comment> getComments(UUID postId);

void deleteComments(List<Comment> comments);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.sparta.board.comment.application.usecase;

import io.sparta.board.comment.model.entity.Comment;
import io.sparta.board.comment.model.repository.CommentRepository;
import io.sparta.board.comment.presentation.dto.request.CommentUpdateRequestDto;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentServiceImpl implements CommentService {

private final CommentRepository commentRepository;

@Transactional
@Override
public Comment createComment(Comment comment) {
return commentRepository.save(comment);
}

@Override
public Comment getComment(UUID commentId) {
return commentRepository.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));
}

@Transactional
@Override
public void updateComment(Comment comment, CommentUpdateRequestDto requestDto) {
requestDto.updateComment(comment);
}

@Override
public void isDeleted(Comment comment) {
if (comment.isDeleted()) {
throw new IllegalArgumentException("삭제된 댓글입니다.");
}
}

@Transactional
@Override
public void deleteComment(Comment comment) {
comment.delete();
}

@Override
public List<Comment> getComments(UUID postId, Pageable pageable) {
if (!pageSizeCheck(pageable.getPageSize())) {
pageable = PageRequest.of(pageable.getPageNumber(), 10, pageable.getSort());
}
return commentRepository.findByPostIdAndDeletedFalse(postId, pageable);
}

@Override
public List<Comment> getComments(UUID postId) {
return commentRepository.findByPostIdAndDeletedFalse(postId);
}

@Transactional
@Override
public void deleteComments(List<Comment> comments) {
for (Comment comment : comments) {
comment.delete();
}
}

private boolean pageSizeCheck(int size) {
return size == 10 || size == 30 || size == 50;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.sparta.board.comment.infrastructure.repository;

import io.sparta.board.comment.model.entity.Comment;
import io.sparta.board.comment.model.repository.CommentRepository;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaCommentRepository extends JpaRepository<Comment, UUID>, CommentRepository {

}
53 changes: 53 additions & 0 deletions src/main/java/io/sparta/board/comment/model/entity/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.sparta.board.comment.model.entity;

import io.sparta.board.common.auditing.BaseEntity;
import io.sparta.board.post.model.entity.Post;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "p_comment")
@NoArgsConstructor
@Getter
@Entity
public class Comment extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(nullable = false, columnDefinition = "TEXT")
private String content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;

@Builder(access = AccessLevel.PRIVATE)
private Comment(String content, Post post) {
this.content = content;
this.post = post;
}

public static Comment createComment(String content, Post post) {
return Comment.builder()
.content(content)
.post(post)
.build();
}

public void updateComment(String content) {
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sparta.board.comment.model.repository;

import io.sparta.board.comment.model.entity.Comment;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Pageable;

public interface CommentRepository {

Comment save(Comment comment);

Optional<Comment> findById(UUID commentId);

List<Comment> findByPostIdAndDeletedFalse(UUID postId, Pageable pageable);

List<Comment> findByPostIdAndDeletedFalse(UUID postId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.sparta.board.comment.presentation.controller;

import io.sparta.board.comment.application.facade.CommentFacade;
import io.sparta.board.comment.presentation.dto.request.CommentCreateRequestDto;
import io.sparta.board.comment.presentation.dto.request.CommentUpdateRequestDto;
import io.sparta.board.comment.presentation.dto.response.CommentResponseDto;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/comments")
public class CommentController {

private final CommentFacade commentFacade;

@PostMapping("/{id}")
public ResponseEntity<CommentResponseDto> createComment(
@Valid
@RequestBody CommentCreateRequestDto requestDto,
@PathVariable("id") UUID postId
) {
return ResponseEntity.ok(commentFacade.createComment(requestDto, postId));
}

@PatchMapping("/{id}")
public ResponseEntity<CommentResponseDto> updateComment(
@Valid
@RequestBody CommentUpdateRequestDto requestDto,
@PathVariable("id") UUID commentId
) {
return ResponseEntity.ok(commentFacade.updateComment(commentId, requestDto));
}

@DeleteMapping("/{id}")
public void deleteComment(
@PathVariable("id") UUID commentId
) {
commentFacade.deleteComment(commentId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.sparta.board.comment.presentation.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.sparta.board.comment.model.entity.Comment;
import io.sparta.board.post.model.entity.Post;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public class CommentCreateRequestDto {

@JsonProperty
@NotBlank(message = "내용을 입력해주세요.")
@Size(min = 10, message = "내용은 10자 이상 입력해주세요.")
private final String content;

public Comment createComment(Post post) {
return Comment.createComment(content, post);
}

}
Loading