diff --git a/src/main/java/org/sopt/Main.java b/src/main/java/org/sopt/Main.java index 58ce0e7..a876d9c 100644 --- a/src/main/java/org/sopt/Main.java +++ b/src/main/java/org/sopt/Main.java @@ -5,7 +5,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication -@EnableJpaAuditing public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); diff --git a/src/main/java/org/sopt/common/config/JpaAuditingConfig.java b/src/main/java/org/sopt/common/config/JpaAuditingConfig.java new file mode 100644 index 0000000..e6357be --- /dev/null +++ b/src/main/java/org/sopt/common/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package org.sopt.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/src/main/java/org/sopt/common/entity/BaseEntity.java b/src/main/java/org/sopt/common/entity/BaseEntity.java new file mode 100644 index 0000000..e9eeb98 --- /dev/null +++ b/src/main/java/org/sopt/common/entity/BaseEntity.java @@ -0,0 +1,23 @@ +package org.sopt.common.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/org/sopt/common/entity/BaseSoftDeleteEntity.java b/src/main/java/org/sopt/common/entity/BaseSoftDeleteEntity.java new file mode 100644 index 0000000..00018e7 --- /dev/null +++ b/src/main/java/org/sopt/common/entity/BaseSoftDeleteEntity.java @@ -0,0 +1,19 @@ +package org.sopt.common.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@MappedSuperclass +public abstract class BaseSoftDeleteEntity extends BaseEntity { + + private LocalDateTime deletedAt; + + @Column(nullable = false) + private Boolean isDeleted = false; + + public void delete() { + this.deletedAt = LocalDateTime.now(); + this.isDeleted = true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/common/exception/CommentException.java b/src/main/java/org/sopt/common/exception/CommentException.java new file mode 100644 index 0000000..fac0563 --- /dev/null +++ b/src/main/java/org/sopt/common/exception/CommentException.java @@ -0,0 +1,10 @@ +package org.sopt.common.exception; + +import org.sopt.common.response.ErrorCode; + +public class CommentException extends GeneralException { + + public CommentException(ErrorCode errorCode) { + super(errorCode); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/common/response/ErrorCode.java b/src/main/java/org/sopt/common/response/ErrorCode.java index 47fe461..fca6fd4 100644 --- a/src/main/java/org/sopt/common/response/ErrorCode.java +++ b/src/main/java/org/sopt/common/response/ErrorCode.java @@ -30,7 +30,11 @@ public enum ErrorCode { // Article INVALID_TAG(3001, HttpStatus.BAD_REQUEST, "❌ 유효하지 않은 태그값입니다."), ARTICLE_NOT_FOUND(3002, HttpStatus.NOT_FOUND, "❌ 존재하지 않는 아티클입니다."), - DUPLICATE_ARTICLE_TITLE(3003, HttpStatus.BAD_REQUEST, "⚠️ 이미 존재하는 아티클 제목입니다.") + DUPLICATE_ARTICLE_TITLE(3003, HttpStatus.BAD_REQUEST, "⚠️ 이미 존재하는 아티클 제목입니다."), + + // Comment + COMMENT_NOT_FOUND(4001, HttpStatus.NOT_FOUND, "❌ 존재하지 않는 댓글입니다."), + COMMENT_EDIT_NOT_ALLOWED(4002, HttpStatus.FORBIDDEN, "❌댓글 작성자만 수정할 수 있습니다.") ; private final int code; diff --git a/src/main/java/org/sopt/common/response/SuccessCode.java b/src/main/java/org/sopt/common/response/SuccessCode.java index 9faf502..5d9ee3a 100644 --- a/src/main/java/org/sopt/common/response/SuccessCode.java +++ b/src/main/java/org/sopt/common/response/SuccessCode.java @@ -15,7 +15,12 @@ public enum SuccessCode { // Article ARTICLE_CREATED(3001, HttpStatus.CREATED, "✅ 아티클이 성공적으로 등록되었습니다."), - ARTICLE_FOUND(3002, HttpStatus.OK, "✅ 아티클이 성공적으로 조회되었습니다.") + ARTICLE_FOUND(3002, HttpStatus.OK, "✅ 아티클이 성공적으로 조회되었습니다."), + + // Comment + COMMENT_CREATED(4001, HttpStatus.CREATED, "✅ 댓글이 성공적으로 등록되었습니다."), + COMMENT_FOUND(4002, HttpStatus.OK, "✅ 댓글이 성공적으로 조회되었습니다."), + COMMENT_DELETED(4003, HttpStatus.OK, "✅ 댓글이 성공적으로 삭제되었습니다.") ; private final int code; diff --git a/src/main/java/org/sopt/domain/article/controller/ArticleController.java b/src/main/java/org/sopt/domain/article/controller/ArticleController.java index 615fdb7..d9d0fac 100644 --- a/src/main/java/org/sopt/domain/article/controller/ArticleController.java +++ b/src/main/java/org/sopt/domain/article/controller/ArticleController.java @@ -4,9 +4,13 @@ import lombok.RequiredArgsConstructor; import org.sopt.common.response.ApiResponse; import org.sopt.common.response.SuccessCode; -import org.sopt.domain.article.dto.request.ArticleCreateRequestDTO; -import org.sopt.domain.article.dto.response.ArticleResponseDTO; +import org.sopt.domain.article.dto.request.ArticleCreateRequest; +import org.sopt.domain.article.dto.request.CommentCreateRequest; +import org.sopt.domain.article.dto.response.ArticleResponse; +import org.sopt.domain.article.dto.response.CommentListResponse; +import org.sopt.domain.article.dto.response.CommentResponse; import org.sopt.domain.article.service.ArticleService; +import org.sopt.domain.article.service.CommentService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,30 +22,55 @@ public class ArticleController { private final ArticleService articleService; + private final CommentService commentService; // ⚪ 아티클 생성 API @PostMapping - public ResponseEntity> createArticle( - @Valid @RequestBody ArticleCreateRequestDTO articleCreateRequestDTO + public ResponseEntity> createArticle( + @Valid @RequestBody ArticleCreateRequest articleCreateRequest ) { - ArticleResponseDTO responseDTO = articleService.createArticle(articleCreateRequestDTO); + ArticleResponse response = articleService.createArticle(articleCreateRequest); return ResponseEntity.status(SuccessCode.ARTICLE_CREATED.getStatus()) - .body(ApiResponse.ok(SuccessCode.ARTICLE_CREATED, responseDTO)); + .body(ApiResponse.ok(SuccessCode.ARTICLE_CREATED, response)); } // ⚪ 아티클 단일 조회 API @GetMapping("/{articleId}") - public ResponseEntity> getArticle(@PathVariable Long articleId) { - ArticleResponseDTO responseDTO = articleService.getArticleById(articleId); + public ResponseEntity> getArticle(@PathVariable Long articleId) { + ArticleResponse response = articleService.getArticleById(articleId); return ResponseEntity.status(SuccessCode.ARTICLE_FOUND.getStatus()) - .body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, responseDTO)); + .body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, response)); } // ⚪ 아티클 전체 조회 API @GetMapping - public ResponseEntity>> getAllArticles() { - List responseDTOs = articleService.getArticles(); + public ResponseEntity>> getAllArticles() { + List response = articleService.getArticles(); return ResponseEntity.status(SuccessCode.ARTICLE_FOUND.getStatus()) - .body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, responseDTOs)); + .body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, response)); + } + + // ⚪ 댓글 작성 API + @PostMapping("{articleId}/comments") + public ResponseEntity> createComment( + @RequestHeader("Member-Id") Long memberId, + @PathVariable Long articleId, + @Valid @RequestBody CommentCreateRequest request + ) { + CommentResponse response = commentService.createComment(memberId, articleId, request); + return ResponseEntity + .status(SuccessCode.COMMENT_CREATED.getStatus()) + .body(ApiResponse.ok(SuccessCode.COMMENT_CREATED, response)); + } + + // ⚪ 특정 아티클의 댓글 전체 조회 API + @GetMapping("{articleId}/comments") + public ResponseEntity> readComments( + @PathVariable Long articleId + ) { + CommentListResponse response = commentService.readComments(articleId); + return ResponseEntity + .status(SuccessCode.COMMENT_FOUND.getStatus()) + .body(ApiResponse.ok(SuccessCode.COMMENT_FOUND, response)); } } diff --git a/src/main/java/org/sopt/domain/article/controller/CommentController.java b/src/main/java/org/sopt/domain/article/controller/CommentController.java new file mode 100644 index 0000000..9eef8fe --- /dev/null +++ b/src/main/java/org/sopt/domain/article/controller/CommentController.java @@ -0,0 +1,44 @@ +package org.sopt.domain.article.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.sopt.common.response.ApiResponse; +import org.sopt.common.response.SuccessCode; +import org.sopt.domain.article.dto.request.CommentUpdateRequest; +import org.sopt.domain.article.dto.response.CommentResponse; +import org.sopt.domain.article.service.CommentService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/comments") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + // ⚪ 댓글 수정 API + @PatchMapping("/{commentId}") + public ResponseEntity> updateComment( + @RequestHeader("Member-Id") Long memberId, + @PathVariable Long commentId, + @Valid @RequestBody CommentUpdateRequest request + ) { + CommentResponse response = commentService.updateComment(memberId, commentId, request); + return ResponseEntity + .status(SuccessCode.COMMENT_CREATED.getStatus()) + .body(ApiResponse.ok(SuccessCode.COMMENT_CREATED, response)); + } + + // ⚪ 댓글 삭제 API + @DeleteMapping("/{commentId}") + public ResponseEntity> deleteComment( + @RequestHeader("Member-Id") Long memberId, + @PathVariable Long commentId + ) { + commentService.deleteComment(memberId, commentId); + return ResponseEntity + .status(SuccessCode.COMMENT_DELETED.getStatus()) + .body(ApiResponse.ok(SuccessCode.COMMENT_DELETED, null)); + } +} diff --git a/src/main/java/org/sopt/domain/article/dto/request/ArticleCreateRequestDTO.java b/src/main/java/org/sopt/domain/article/dto/request/ArticleCreateRequest.java similarity index 94% rename from src/main/java/org/sopt/domain/article/dto/request/ArticleCreateRequestDTO.java rename to src/main/java/org/sopt/domain/article/dto/request/ArticleCreateRequest.java index 141adf7..8aeb586 100644 --- a/src/main/java/org/sopt/domain/article/dto/request/ArticleCreateRequestDTO.java +++ b/src/main/java/org/sopt/domain/article/dto/request/ArticleCreateRequest.java @@ -5,7 +5,7 @@ import org.sopt.domain.article.entity.Article; import org.sopt.domain.article.entity.ArticleTag; -public record ArticleCreateRequestDTO( +public record ArticleCreateRequest( @NotNull(message = "회원 ID는 필수입니다.") Long memberId, diff --git a/src/main/java/org/sopt/domain/article/dto/request/CommentCreateRequest.java b/src/main/java/org/sopt/domain/article/dto/request/CommentCreateRequest.java new file mode 100644 index 0000000..c1a61a3 --- /dev/null +++ b/src/main/java/org/sopt/domain/article/dto/request/CommentCreateRequest.java @@ -0,0 +1,11 @@ +package org.sopt.domain.article.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentCreateRequest( + @NotBlank(message = "내용은 필수입니다.") + @Size(max = 300, message = "내용은 300자 이내여야 합니다.") + String content +) { +} diff --git a/src/main/java/org/sopt/domain/article/dto/request/CommentUpdateRequest.java b/src/main/java/org/sopt/domain/article/dto/request/CommentUpdateRequest.java new file mode 100644 index 0000000..4c390ae --- /dev/null +++ b/src/main/java/org/sopt/domain/article/dto/request/CommentUpdateRequest.java @@ -0,0 +1,11 @@ +package org.sopt.domain.article.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentUpdateRequest( + @NotBlank(message = "내용은 필수입니다.") + @Size(max = 300, message = "내용은 300자 이내여야 합니다.") + String content +) { +} diff --git a/src/main/java/org/sopt/domain/article/dto/response/ArticleResponseDTO.java b/src/main/java/org/sopt/domain/article/dto/response/ArticleResponse.java similarity index 70% rename from src/main/java/org/sopt/domain/article/dto/response/ArticleResponseDTO.java rename to src/main/java/org/sopt/domain/article/dto/response/ArticleResponse.java index a81757f..eab4a51 100644 --- a/src/main/java/org/sopt/domain/article/dto/response/ArticleResponseDTO.java +++ b/src/main/java/org/sopt/domain/article/dto/response/ArticleResponse.java @@ -3,17 +3,17 @@ import org.sopt.domain.article.entity.Article; import org.sopt.domain.article.entity.ArticleTag; -public record ArticleResponseDTO( +public record ArticleResponse( Long articleId, String memberName, String title, String content, ArticleTag tag ) { - public static ArticleResponseDTO of(Article article) { - return new ArticleResponseDTO( + public static ArticleResponse of(Article article) { + return new ArticleResponse( article.getId(), - article.getMember().getName(), + article.getAuthor().getName(), article.getTitle(), article.getContent(), article.getTag() diff --git a/src/main/java/org/sopt/domain/article/dto/response/CommentListResponse.java b/src/main/java/org/sopt/domain/article/dto/response/CommentListResponse.java new file mode 100644 index 0000000..090e57d --- /dev/null +++ b/src/main/java/org/sopt/domain/article/dto/response/CommentListResponse.java @@ -0,0 +1,17 @@ +package org.sopt.domain.article.dto.response; + +import org.sopt.domain.article.entity.Comment; +import java.util.List; + +public record CommentListResponse( + List comments +) { + public static CommentListResponse of(List comments) { + return new CommentListResponse( + comments.stream() + .map(CommentResponse::of) + .toList() + ); + } + +} \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/article/dto/response/CommentResponse.java b/src/main/java/org/sopt/domain/article/dto/response/CommentResponse.java new file mode 100644 index 0000000..4d75c01 --- /dev/null +++ b/src/main/java/org/sopt/domain/article/dto/response/CommentResponse.java @@ -0,0 +1,22 @@ +package org.sopt.domain.article.dto.response; + +import org.sopt.domain.article.entity.Comment; +import java.time.LocalDateTime; + +public record CommentResponse( + Long commentId, + String content, + Long authorId, + String authorName, + LocalDateTime createdAt +) { + public static CommentResponse of(Comment comment) { + return new CommentResponse( + comment.getId(), + comment.getContent(), + comment.getAuthor().getId(), + comment.getAuthor().getName(), + comment.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/article/entity/Article.java b/src/main/java/org/sopt/domain/article/entity/Article.java index 8973614..abaee52 100644 --- a/src/main/java/org/sopt/domain/article/entity/Article.java +++ b/src/main/java/org/sopt/domain/article/entity/Article.java @@ -4,23 +4,15 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.sopt.common.entity.BaseEntity; import org.sopt.domain.member.entity.Member; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(AuditingEntityListener.class) -@Table( - name = "article", - uniqueConstraints = { - @UniqueConstraint(columnNames = {"title"}) - } -) -public class Article { +public class Article extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "article_id") @@ -36,13 +28,12 @@ public class Article { @Column(name = "tag", nullable = false) private ArticleTag tag; - @CreatedDate - @Column(name = "created_date", nullable = false) - private LocalDate createdDate; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "memberId") - private Member member; + @JoinColumn(name = "member_id") + private Member author; + + @OneToMany(mappedBy = "article") + private List comments = new ArrayList<>(); public Article(String title, String content, ArticleTag tag) { this.title = title; @@ -53,8 +44,4 @@ public Article(String title, String content, ArticleTag tag) { public static Article create(String title, String content, ArticleTag tag) { return new Article(title, content, tag); } - - public void connectMember(Member member) { - this.member = member; - } } \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/article/entity/Comment.java b/src/main/java/org/sopt/domain/article/entity/Comment.java new file mode 100644 index 0000000..18ee2d6 --- /dev/null +++ b/src/main/java/org/sopt/domain/article/entity/Comment.java @@ -0,0 +1,49 @@ +package org.sopt.domain.article.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; +import org.sopt.common.entity.BaseSoftDeleteEntity; +import org.sopt.domain.member.entity.Member; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE comment SET is_deleted = true, deleted_at = NOW() WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Comment extends BaseSoftDeleteEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long id; + + @Column(name = "content", nullable = false, length = 300) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member author; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id") + private Article article; + + public static Comment create(Member author, Article article, String content) { + Comment comment = new Comment(author, article, content); + article.getComments().add(comment); + return comment; + } + + private Comment(Member author, Article article, String content) { + this.author = author; + this.article = article; + this.content = content; + } + + public void updateContent(String content) { + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/article/repository/CommentRepository.java b/src/main/java/org/sopt/domain/article/repository/CommentRepository.java new file mode 100644 index 0000000..dff5e28 --- /dev/null +++ b/src/main/java/org/sopt/domain/article/repository/CommentRepository.java @@ -0,0 +1,19 @@ +package org.sopt.domain.article.repository; + +import org.sopt.domain.article.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface CommentRepository extends JpaRepository { + + @Query("select c " + + "from Comment c " + + "join fetch c.author " + + "where c.article.id = :articleId " + + "order by c.createdAt desc") + List findByArticleIdWithAuthor(@Param("articleId") Long articleId); +} \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/article/service/ArticleService.java b/src/main/java/org/sopt/domain/article/service/ArticleService.java index 5964449..3a10cc9 100644 --- a/src/main/java/org/sopt/domain/article/service/ArticleService.java +++ b/src/main/java/org/sopt/domain/article/service/ArticleService.java @@ -4,8 +4,8 @@ import org.sopt.common.exception.ArticleException; import org.sopt.common.exception.MemberException; import org.sopt.common.response.ErrorCode; -import org.sopt.domain.article.dto.request.ArticleCreateRequestDTO; -import org.sopt.domain.article.dto.response.ArticleResponseDTO; +import org.sopt.domain.article.dto.request.ArticleCreateRequest; +import org.sopt.domain.article.dto.response.ArticleResponse; import org.sopt.domain.article.entity.Article; import org.sopt.domain.article.repository.ArticleRepository; import org.sopt.domain.member.entity.Member; @@ -25,31 +25,30 @@ public class ArticleService { private final MemberRepository memberRepository; @Transactional - public ArticleResponseDTO createArticle(ArticleCreateRequestDTO requestDTO) { - Member member = memberRepository.findById(requestDTO.memberId()) + public ArticleResponse createArticle(ArticleCreateRequest request) { + Member member = memberRepository.findById(request.memberId()) .orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND)); - if (articleRepository.existsByTitle(requestDTO.title())) { + if (articleRepository.existsByTitle(request.title())) { throw new ArticleException(ErrorCode.DUPLICATE_ARTICLE_TITLE); } - Article article = requestDTO.toEntity(); - member.addArticle(article); + Article article = request.toEntity(); articleRepository.save(article); - return ArticleResponseDTO.of(article); + return ArticleResponse.of(article); } - public ArticleResponseDTO getArticleById(Long articleId) { + public ArticleResponse getArticleById(Long articleId) { Article article = articleRepository.findById(articleId) .orElseThrow(() -> new ArticleException(ErrorCode.ARTICLE_NOT_FOUND)); - return ArticleResponseDTO.of(article); + return ArticleResponse.of(article); } - public List getArticles() { + public List getArticles() { List
articles = articleRepository.findAll(); return articles.stream() - .map(ArticleResponseDTO::of) + .map(ArticleResponse::of) .collect(Collectors.toList()); } } \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/article/service/CommentService.java b/src/main/java/org/sopt/domain/article/service/CommentService.java new file mode 100644 index 0000000..e5d43ac --- /dev/null +++ b/src/main/java/org/sopt/domain/article/service/CommentService.java @@ -0,0 +1,69 @@ +package org.sopt.domain.article.service; + +import lombok.RequiredArgsConstructor; +import org.sopt.common.exception.ArticleException; +import org.sopt.common.exception.CommentException; +import org.sopt.common.exception.MemberException; +import org.sopt.common.response.ErrorCode; +import org.sopt.domain.article.dto.request.CommentCreateRequest; +import org.sopt.domain.article.dto.request.CommentUpdateRequest; +import org.sopt.domain.article.dto.response.CommentListResponse; +import org.sopt.domain.article.dto.response.CommentResponse; +import org.sopt.domain.article.entity.Article; +import org.sopt.domain.article.entity.Comment; +import org.sopt.domain.article.repository.ArticleRepository; +import org.sopt.domain.article.repository.CommentRepository; +import org.sopt.domain.member.entity.Member; +import org.sopt.domain.member.repository.MemberRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentService { + + private final MemberRepository memberRepository; + private final ArticleRepository articleRepository; + private final CommentRepository commentRepository; + + @Transactional + public CommentResponse createComment(Long memberId, Long articleId, CommentCreateRequest request) { + Member author = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND)); + + Article article = articleRepository.findById(articleId) + .orElseThrow(() -> new ArticleException(ErrorCode.ARTICLE_NOT_FOUND)); + + Comment comment = Comment.create(author, article, request.content()); + commentRepository.save(comment); + return CommentResponse.of(comment); + } + + public CommentListResponse readComments(Long articleId) { + List comments = commentRepository.findByArticleIdWithAuthor(articleId); + return CommentListResponse.of(comments); + } + + @Transactional + public CommentResponse updateComment(Long memberId, Long commentId, CommentUpdateRequest request) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new CommentException(ErrorCode.COMMENT_NOT_FOUND)); + + if (!comment.getAuthor().getId().equals(memberId)) + throw new CommentException(ErrorCode.COMMENT_EDIT_NOT_ALLOWED); + + comment.updateContent(request.content()); + return CommentResponse.of(comment); + } + + @Transactional + public void deleteComment(Long memberId, Long commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new CommentException(ErrorCode.COMMENT_NOT_FOUND)); + if (!comment.getAuthor().getId().equals(memberId)) + throw new CommentException(ErrorCode.COMMENT_EDIT_NOT_ALLOWED); + comment.delete(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/member/controller/MemberController.java b/src/main/java/org/sopt/domain/member/controller/MemberController.java index c0829c4..05a0080 100644 --- a/src/main/java/org/sopt/domain/member/controller/MemberController.java +++ b/src/main/java/org/sopt/domain/member/controller/MemberController.java @@ -3,8 +3,8 @@ import jakarta.validation.Valid; import org.sopt.common.response.ApiResponse; import org.sopt.common.response.SuccessCode; -import org.sopt.domain.member.dto.request.MemberCreateRequestDTO; -import org.sopt.domain.member.dto.response.MemberResponseDTO; +import org.sopt.domain.member.dto.request.MemberCreateRequest; +import org.sopt.domain.member.dto.response.MemberResponse; import org.sopt.domain.member.service.MemberService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -21,27 +21,26 @@ public MemberController(MemberService memberService) { } @PostMapping - public ResponseEntity> createMember( - @RequestBody @Valid - MemberCreateRequestDTO requestDTO + public ResponseEntity> createMember( + @RequestBody @Valid MemberCreateRequest request ) { - MemberResponseDTO responseDTO = memberService.join(requestDTO); + MemberResponse response = memberService.join(request); return ResponseEntity.status(SuccessCode.MEMBER_CREATED.getStatus()) - .body(ApiResponse.ok(SuccessCode.MEMBER_CREATED, responseDTO)); + .body(ApiResponse.ok(SuccessCode.MEMBER_CREATED, response)); } @GetMapping("/{memberId}") - public ResponseEntity> findMemberById(@PathVariable Long memberId) { - MemberResponseDTO responseDTO = memberService.findOne(memberId); + public ResponseEntity> findMemberById(@PathVariable Long memberId) { + MemberResponse response = memberService.findOne(memberId); return ResponseEntity.status(SuccessCode.MEMBER_FOUND.getStatus()) - .body(ApiResponse.ok(SuccessCode.MEMBER_FOUND, responseDTO)); + .body(ApiResponse.ok(SuccessCode.MEMBER_FOUND, response)); } @GetMapping - public ResponseEntity>> getAllMembers() { - List responseDTOs = memberService.findAllMembers(); + public ResponseEntity>> getAllMembers() { + List response = memberService.findAllMembers(); return ResponseEntity.status(SuccessCode.MEMBER_FOUND.getStatus()) - .body(ApiResponse.ok(SuccessCode.MEMBER_FOUND, responseDTOs)); + .body(ApiResponse.ok(SuccessCode.MEMBER_FOUND, response)); } @DeleteMapping("/{memberId}") diff --git a/src/main/java/org/sopt/domain/member/dto/request/MemberCreateRequestDTO.java b/src/main/java/org/sopt/domain/member/dto/request/MemberCreateRequest.java similarity index 95% rename from src/main/java/org/sopt/domain/member/dto/request/MemberCreateRequestDTO.java rename to src/main/java/org/sopt/domain/member/dto/request/MemberCreateRequest.java index fc5a134..6799d9a 100644 --- a/src/main/java/org/sopt/domain/member/dto/request/MemberCreateRequestDTO.java +++ b/src/main/java/org/sopt/domain/member/dto/request/MemberCreateRequest.java @@ -8,7 +8,7 @@ import java.time.LocalDate; -public record MemberCreateRequestDTO( +public record MemberCreateRequest( @NotBlank(message = "이름은 필수입니다.") String name, diff --git a/src/main/java/org/sopt/domain/member/dto/response/MemberResponseDTO.java b/src/main/java/org/sopt/domain/member/dto/response/MemberResponse.java similarity index 78% rename from src/main/java/org/sopt/domain/member/dto/response/MemberResponseDTO.java rename to src/main/java/org/sopt/domain/member/dto/response/MemberResponse.java index d9a4a86..7bef3e2 100644 --- a/src/main/java/org/sopt/domain/member/dto/response/MemberResponseDTO.java +++ b/src/main/java/org/sopt/domain/member/dto/response/MemberResponse.java @@ -4,15 +4,15 @@ import org.sopt.domain.member.entity.Member; import java.time.LocalDate; -public record MemberResponseDTO( +public record MemberResponse( Long memberId, String name, LocalDate birthdate, String email, Gender gender ) { - public static MemberResponseDTO of(Member member) { - return new MemberResponseDTO( + public static MemberResponse of(Member member) { + return new MemberResponse( member.getId(), member.getName(), member.getBirthdate(), diff --git a/src/main/java/org/sopt/domain/member/entity/Member.java b/src/main/java/org/sopt/domain/member/entity/Member.java index a388e1b..2874ef1 100644 --- a/src/main/java/org/sopt/domain/member/entity/Member.java +++ b/src/main/java/org/sopt/domain/member/entity/Member.java @@ -4,11 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.domain.article.entity.Article; - import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; @Entity @Getter @@ -22,9 +18,6 @@ public class Member { private String email; private Gender gender; - @OneToMany(mappedBy = "member") - private List
articles = new ArrayList<>(); - public Member(String name, LocalDate birthdate, String email, Gender gender) { this.name = name; this.birthdate = birthdate; @@ -36,8 +29,4 @@ public static Member create(String name, LocalDate birthdate, String email, Gend return new Member(name, birthdate, email, gender); } - public void addArticle(Article article) { - this.articles.add(article); - article.connectMember(this); - } } \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/member/service/MemberService.java b/src/main/java/org/sopt/domain/member/service/MemberService.java index d9306ce..3924429 100644 --- a/src/main/java/org/sopt/domain/member/service/MemberService.java +++ b/src/main/java/org/sopt/domain/member/service/MemberService.java @@ -1,16 +1,16 @@ package org.sopt.domain.member.service; -import org.sopt.domain.member.dto.request.MemberCreateRequestDTO; -import org.sopt.domain.member.dto.response.MemberResponseDTO; +import org.sopt.domain.member.dto.request.MemberCreateRequest; +import org.sopt.domain.member.dto.response.MemberResponse; import java.util.List; public interface MemberService { - MemberResponseDTO join(MemberCreateRequestDTO requestDTO); + MemberResponse join(MemberCreateRequest request); - MemberResponseDTO findOne(Long memberId); + MemberResponse findOne(Long memberId); - List findAllMembers(); + List findAllMembers(); void delete(Long memberId); } \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/member/service/MemberServiceImpl.java b/src/main/java/org/sopt/domain/member/service/MemberServiceImpl.java index b21326b..034a494 100644 --- a/src/main/java/org/sopt/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/org/sopt/domain/member/service/MemberServiceImpl.java @@ -4,8 +4,8 @@ import org.sopt.common.exception.MemberException; import org.sopt.domain.member.entity.Gender; import org.sopt.domain.member.entity.Member; -import org.sopt.domain.member.dto.request.MemberCreateRequestDTO; -import org.sopt.domain.member.dto.response.MemberResponseDTO; +import org.sopt.domain.member.dto.request.MemberCreateRequest; +import org.sopt.domain.member.dto.response.MemberResponse; import org.sopt.domain.member.repository.MemberRepository; import org.sopt.domain.member.validator.MemberValidator; import org.springframework.stereotype.Service; @@ -22,12 +22,12 @@ public MemberServiceImpl(MemberRepository memberRepository) { } @Override - public MemberResponseDTO join(MemberCreateRequestDTO requestDTO) { - String name = requestDTO.name(); - LocalDate birthdate = requestDTO.birthdate(); + public MemberResponse join(MemberCreateRequest request) { + String name = request.name(); + LocalDate birthdate = request.birthdate(); MemberValidator.validateAge(birthdate); - String email = requestDTO.email(); - Gender gender = requestDTO.gender(); + String email = request.email(); + Gender gender = request.gender(); if (memberRepository.existsByEmail(email)) { throw new MemberException(ErrorCode.DUPLICATE_EMAIL); @@ -35,21 +35,21 @@ public MemberResponseDTO join(MemberCreateRequestDTO requestDTO) { Member member = Member.create(name, birthdate, email, gender); memberRepository.save(member); - return MemberResponseDTO.of(member); + return MemberResponse.of(member); } @Override - public MemberResponseDTO findOne(Long memberId) { + public MemberResponse findOne(Long memberId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND)); - return MemberResponseDTO.of(member); + return MemberResponse.of(member); } @Override - public List findAllMembers() { + public List findAllMembers() { List members = memberRepository.findAll(); return members.stream() - .map(MemberResponseDTO::of) + .map(MemberResponse::of) .toList(); }