Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b7a4272
feat: BaseEntity 구현
yejiscore Apr 1, 2025
918fdb6
feat: 게시글 Entity 구현
yejiscore Apr 1, 2025
c0b2580
feat: 댓글 Entity 구현
yejiscore Apr 1, 2025
50f937f
feat: 유효성 검사를 위한 의존성 추가
yejiscore Apr 1, 2025
4daf00f
feat: 게시글 DTO 구현
yejiscore Apr 1, 2025
5ac0203
feat: 댓글 DTO 구현
yejiscore Apr 1, 2025
e39a01e
feat: 게시글, 댓글 mapper 구현
yejiscore Apr 1, 2025
2e87ba0
refactor: 게시글, 댓글 도메인 분리
yejiscore Apr 1, 2025
f442700
feat: BaseEntity 구현
yejiscore Apr 1, 2025
bf4649b
refactor: 도메인 분리 후 import 경로 수정
yejiscore Apr 1, 2025
2577930
feat: DB 설정
yejiscore Apr 2, 2025
a0e31ea
feat: 게시글 생성 구현
yejiscore Apr 2, 2025
59773ef
feat: @AllArgsConstructor 어노테이션 추가
yejiscore Apr 2, 2025
0592378
test: 게시글 생성 테스트코드 작성
yejiscore Apr 2, 2025
e80c14d
feat: 게시글 전체 조회, 단건 조회 구현
yejiscore Apr 2, 2025
7d8793f
feat: 게시글 수정 구현
yejiscore Apr 2, 2025
825d6fa
feat: 유효성 검사 어노테이션 추가
yejiscore Apr 2, 2025
9db853c
feat: 게시글 soft delete 구현
yejiscore Apr 2, 2025
4ff7b9d
feat: soft delete 된 게시글 제외하고 조회할 수 있도록 설정
yejiscore Apr 2, 2025
1e68581
feat: soft delete 된 댓글 제외하고 조회할 수 있도록 설정
yejiscore Apr 2, 2025
6cdec11
feat: AllArgsConstructor 어노테이션 추가
yejiscore Apr 2, 2025
f3b103c
feat: 댓글 생성 구현
yejiscore Apr 2, 2025
cee3801
feat: 특정 게시글에 작성된 댓글 전체 조회 구현
yejiscore Apr 2, 2025
cf8f0d3
feat: 댓글 수정 구현
yejiscore Apr 2, 2025
686cf28
feat: 댓글 soft delete 구현
yejiscore Apr 2, 2025
a8743c0
refactor: BaseEntity를 common 에서 관리
yejiscore Apr 2, 2025
7a3ed2c
test: 테스트코드 작성
yejiscore Apr 2, 2025
c74902a
feat: 공통 예외처리 구현
yejiscore Apr 2, 2025
5761fd9
refactor: 공통 예외처리 적용
yejiscore Apr 2, 2025
2059723
feat: 게시글 삭제 시 해당 게시글의 댓글도 함께 삭제
yejiscore Apr 2, 2025
f77d580
feat: README TODO 작성
yejiscore 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
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## TODO List
- [X] 패키지 구조 설계


- [X] BaseEntity 설계
- [X] createdAt, updatedAt, deletedAt
- [X] softDelete()


- [X] 게시글 Entity 설계
- [X] postId, postTitle, postContent
- [X] 댓글 Entity 설계
- [X] commentId, commentContent


- [X] 게시글 DTO, mapper 설계
- [X] 댓글 DTO, mapper 설계


- [X] 게시글 생성 API 구현
- [X] 제목, 내용 필수 처리

- [X] 게시글 조회 API 구현
- [X] soft delete 는 제외
- [X] 전체 조회
- [X] 단건 조회
- [ ] search 조회

- [X] 게시글 수정 API 구현
- [X] 현재 존재하는 게시글인지 확인
- [X] 제목, 내용만 수정 가능

- [X] 게시글 삭제 API 구현
- [X] 현재 존재하는 게시글인지 확인
- [X] soft delete 처리

- [X] 게시글 예외 처리
- [X] soft delete 처리되었는지 확인 후 "이미 삭제된 게시글입니다."
- [ ] 아예 존재하지도 않았다면 "존재하지 않는 게시글입니다."

- [ ] 게시글 페이징 적용


- [X] 댓글 생성 API 구현
- [X] 내용 필수 처리

- [X] 댓글 조회 API 구현
- [X] soft delete 는 제외

- [X] 댓글 수정 API 구현
- [X] 현재 존재하는 댓글인지 확인
- [X] 내용만 수정 가능

- [X] 댓글 삭제 API 구현
- [X] 현재 존재하는 댓글인지 확인
- [X] soft delete 처리

- [X] 댓글 예외 처리
- [X] soft delete 처리되었는지 확인 후 "이미 삭제된 댓글입니다."
- [ ] 아예 존재하지도 않았다면 "존재하지 않는 댓글입니다."

- [ ] 댓글 페이징 적용

## 구현 시 어렵거나 이해가 되지 않는 부분
- 상황별 적절한 어노테이션 쓰임새를 헷갈린다. (특히 @AllArgsConstructor, @NoArgsConstructor)
- soft delete 시 엔티티에 조회되지 않도록 설정할 때 where 과 SQLRestriction 차이점이 뭔지 모르겠다.
- 계속 존재하는 게시글/댓글 인지 확인하는 코드가 중복되는데 한번에 처리할 수 있는 방법이 있는지 궁금하다.
- 메세지도 공통 처리가 가능한지 궁금하다.
- 적절한 상태코드를 모르겠다.
- 게시글 삭제할 때 해당 게시글의 댓글도 함께 삭제되는 코드를 모르겠다.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ repositories {
}

dependencies {
implementation 'org.postgresql:postgresql:42.7.1'
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
2 changes: 2 additions & 0 deletions src/main/java/io/sparta/board/BoardApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class BoardApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.sparta.board.comment.application.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CommentRequestDto {

@NotBlank(message = "내용은 필수입니다.")
private String commentContent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.sparta.board.comment.application.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CommentListResponseDto {

private String message;
private int stateCode;
private List<CommentResponseDto.CommentData> comments;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.sparta.board.comment.application.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.UUID;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CommentResponseDto {
private String message;
private int stateCode;
private CommentData comment;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CommentData {
private UUID commentId;
private String commentContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.sparta.board.comment.application.mapper;

import io.sparta.board.comment.application.dto.request.CommentRequestDto;
import io.sparta.board.comment.application.dto.response.CommentResponseDto;
import io.sparta.board.comment.domain.entity.Comment;
import io.sparta.board.post.domain.entity.Post;

public class CommentMapper {

public static Comment toEntity(CommentRequestDto dto, Post post) {
return Comment.builder()
.commentContent(dto.getCommentContent())
.post(post)
.build();
}

public static CommentResponseDto.CommentData toCommentData(Comment comment) {
return CommentResponseDto.CommentData.builder()
.commentId(comment.getCommentId())
.commentContent(comment.getCommentContent())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.sparta.board.comment.application.service;

import io.sparta.board.comment.application.dto.request.CommentRequestDto;
import io.sparta.board.comment.application.dto.response.CommentListResponseDto;
import io.sparta.board.comment.application.dto.response.CommentResponseDto;
import io.sparta.board.comment.application.mapper.CommentMapper;
import io.sparta.board.comment.domain.entity.Comment;
import io.sparta.board.comment.domain.repository.CommentRepository;
import io.sparta.board.common.exception.ErrorCode;
import io.sparta.board.common.exception.GlobalException;
import io.sparta.board.post.domain.entity.Post;
import io.sparta.board.post.domain.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class CommentService {

private final CommentRepository commentRepository;
private final PostRepository postRepository;

@Transactional
public CommentResponseDto createComment(UUID postId, CommentRequestDto dto) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new GlobalException(ErrorCode.POST_NOT_FOUND));

Comment comment = CommentMapper.toEntity(dto, post);
Comment savedComment = commentRepository.save(comment);

return CommentResponseDto.builder()
.message("댓글이 생성되었습니다.")
.stateCode(201)
.comment(CommentMapper.toCommentData(savedComment))
.build();
}

@Transactional
public CommentListResponseDto getCommentsByPostId(UUID postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new GlobalException(ErrorCode.POST_NOT_FOUND));

List<CommentResponseDto.CommentData> comments = commentRepository.findByPost(post).stream()
.map(CommentMapper::toCommentData)
.toList();

return CommentListResponseDto.builder()
.message("댓글이 조회되었습니다.")
.stateCode(200)
.comments(comments)
.build();
}

@Transactional
public CommentResponseDto updateComment(UUID postId, UUID commentId, CommentRequestDto dto) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new GlobalException(ErrorCode.POST_NOT_FOUND));

Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GlobalException(ErrorCode.COMMENT_NOT_FOUND));

comment.update(dto.getCommentContent());

return CommentResponseDto.builder()
.message("댓글이 수정되었습니다.")
.stateCode(200)
.comment(CommentMapper.toCommentData(comment))
.build();
}

@Transactional
public CommentResponseDto deleteComment(UUID postId, UUID commentId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new GlobalException(ErrorCode.POST_NOT_FOUND));

Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GlobalException(ErrorCode.COMMENT_NOT_FOUND));

comment.softDelete();

return CommentResponseDto.builder()
.message("댓글이 삭제되었습니다.")
.stateCode(200)
.build();
}
}
41 changes: 41 additions & 0 deletions src/main/java/io/sparta/board/comment/domain/entity/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.sparta.board.comment.domain.entity;

import io.sparta.board.common.domain.entity.BaseEntity;
import io.sparta.board.post.domain.entity.Post;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;

import java.util.UUID;

@Entity
@Table(name = "p_comment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLRestriction("deleted_at IS NULL")
public class Comment extends BaseEntity {

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

@Column(nullable = false)
private String commentContent;

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

@Builder
public Comment(String commentContent, Post post) {
this.commentContent = commentContent;
this.post = post;
}

public void update(String commentContent) {
this.commentContent = commentContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.sparta.board.comment.domain.repository;

import io.sparta.board.comment.domain.entity.Comment;
import io.sparta.board.post.domain.entity.Post;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface CommentRepository {
Comment save(Comment comment);
List<Comment> findByPost(Post post);
Optional<Comment> findById(UUID commentId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.sparta.board.comment.infrastructure.repository;

import io.sparta.board.comment.domain.entity.Comment;
import io.sparta.board.post.domain.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.UUID;

public interface CommentJpaRepository extends JpaRepository<Comment, UUID> {
List<Comment> findByPost(Post post);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.sparta.board.comment.infrastructure.repository;

import io.sparta.board.comment.domain.entity.Comment;
import io.sparta.board.comment.domain.repository.CommentRepository;
import io.sparta.board.post.domain.entity.Post;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
@RequiredArgsConstructor
public class CommentRepositoryImpl implements CommentRepository {

private final CommentJpaRepository commentJpaRepository;

@Override
public Comment save(Comment comment) {
return commentJpaRepository.save(comment);
}

@Override
public List<Comment> findByPost(Post post) {
return commentJpaRepository.findByPost(post);
}

@Override
public Optional<Comment> findById(UUID commentId) {
return commentJpaRepository.findById(commentId);
}
}
Loading