diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml deleted file mode 100644 index 3d8caac..0000000 --- a/.idea/data_source_mapping.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/main/java/org/sopt/common/ErrorCode.java b/src/main/java/org/sopt/common/ErrorCode.java index ccbebd8..1efd14f 100644 --- a/src/main/java/org/sopt/common/ErrorCode.java +++ b/src/main/java/org/sopt/common/ErrorCode.java @@ -11,7 +11,10 @@ public enum ErrorCode { INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다."), // 아티클 관련 예외 - ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "아티클을 찾을 수 없습니다."); + ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "아티클을 찾을 수 없습니다."), + + // 댓글 관련 예외 + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."),; private final HttpStatus status; private final String message; diff --git a/src/main/java/org/sopt/controller/CommentController.java b/src/main/java/org/sopt/controller/CommentController.java new file mode 100644 index 0000000..5abc1a7 --- /dev/null +++ b/src/main/java/org/sopt/controller/CommentController.java @@ -0,0 +1,49 @@ +package org.sopt.controller; + +import lombok.RequiredArgsConstructor; +import org.sopt.common.ApiResponse; +import org.sopt.dto.request.CommentCreateRequest; +import org.sopt.dto.request.CommentUpdateRequest; +import org.sopt.dto.response.CommentResponse; +import org.sopt.service.CommentService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/comments") +public class CommentController { + + private final CommentService commentService; + + // 댓글 생성 + @PostMapping + public ResponseEntity> createComment(@RequestBody CommentCreateRequest req) { + Long commentId = commentService.create( + req.articleId(), req.memberId(), req.content() + ); + + return ResponseEntity.ok(ApiResponse.success(commentId)); + } + + // 댓글 조회 + @GetMapping("{id}") + public ResponseEntity> getComment(@PathVariable("id") Long id) { + CommentResponse res = commentService.findById(id); + return ResponseEntity.ok(ApiResponse.success(res)); + } + + // 댓글 삭제 + @DeleteMapping("{id}") + public ResponseEntity> deleteComment(@PathVariable("id") Long id) { + commentService.delete(id); + return ResponseEntity.ok(ApiResponse.success(null)); + } + + // 댓글 수정 + @PatchMapping("{id}") + public ResponseEntity> updateComment(@PathVariable("id") Long id, @RequestBody CommentUpdateRequest req) { + CommentResponse res = commentService.update(id, req.content()); + return ResponseEntity.ok(ApiResponse.success(res)); + } +} diff --git a/src/main/java/org/sopt/controller/MemberController.java b/src/main/java/org/sopt/controller/MemberController.java index d03f991..3670b85 100644 --- a/src/main/java/org/sopt/controller/MemberController.java +++ b/src/main/java/org/sopt/controller/MemberController.java @@ -6,7 +6,6 @@ import org.sopt.dto.request.MemberUpdateRequest; import org.sopt.dto.response.MemberResponse; import org.sopt.service.MemberService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -51,7 +50,7 @@ public ResponseEntity> deleteMember(@PathVariable Long memberI } // 회원정보 수정 - @PutMapping("/{memberId}") + @PatchMapping("/{memberId}") public ResponseEntity> updateMember( @PathVariable Long memberId, @Valid @RequestBody MemberUpdateRequest req) { diff --git a/src/main/java/org/sopt/domain/Article.java b/src/main/java/org/sopt/domain/Article.java index 2c2c5e0..f2aa287 100644 --- a/src/main/java/org/sopt/domain/Article.java +++ b/src/main/java/org/sopt/domain/Article.java @@ -1,12 +1,19 @@ package org.sopt.domain; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(indexes = { @Index(name = "idx_article_title", columnList = "title"), @Index(name = "idx_article_member_id", columnList = "memberId") @@ -33,7 +40,8 @@ public class Article { @Column(updatable = false) private LocalDateTime createdAt; - protected Article() {} + @OneToMany(mappedBy="article", cascade=CascadeType.ALL) + private List comments = new ArrayList<>(); private Article(Member member, String title, String content, Tag tag) { this.member = member; @@ -46,28 +54,4 @@ private Article(Member member, String title, String content, Tag tag) { public static Article of(Member member, String title, String content, Tag tag) { return new Article(member, title, content, tag); } - - public Long getId() { - return id; - } - - public Member getMember() { - return member; - } - - public String getTitle() { - return title; - } - - public String getContent() { - return content; - } - - public Tag getTag() { - return tag; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } } diff --git a/src/main/java/org/sopt/domain/Comment.java b/src/main/java/org/sopt/domain/Comment.java new file mode 100644 index 0000000..012d92f --- /dev/null +++ b/src/main/java/org/sopt/domain/Comment.java @@ -0,0 +1,51 @@ +package org.sopt.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public class Comment { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id", nullable = false) + private Article article; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(length = 300) + private String content; + + @CreatedDate + @Column(updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; + + public void update(String content) { + this.content = content; + } + + public static Comment of(Article article, Member member, String content) { + Comment comment = new Comment(); + comment.article = article; + comment.member = member; + comment.content = content; + return comment; + } +} diff --git a/src/main/java/org/sopt/domain/Member.java b/src/main/java/org/sopt/domain/Member.java index 55d7fde..085524e 100644 --- a/src/main/java/org/sopt/domain/Member.java +++ b/src/main/java/org/sopt/domain/Member.java @@ -1,12 +1,17 @@ package org.sopt.domain; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; -import java.io.Serializable; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; @Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(indexes = { @Index(name = "idx_member_name", columnList = "name") }) @@ -16,22 +21,23 @@ public class Member { @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; + @Column(nullable = false) private String name; + @Column(nullable = false) private LocalDate birthDate; + @Column(nullable = false) private String email; + @Column(nullable = false) @Enumerated(EnumType.STRING) private Sex sex; @OneToMany(mappedBy="member", cascade=CascadeType.ALL) - private List
articles; + private List
articles = new ArrayList<>(); - protected Member() {} - - // private 생성자 (외부에서 직접 생성 방지) - private Member(String name, LocalDate birthDate, String email, Sex sex) { + public void update(String name, LocalDate birthDate, String email, Sex sex) { this.name = name; this.birthDate = birthDate; this.email = email; @@ -39,26 +45,11 @@ private Member(String name, LocalDate birthDate, String email, Sex sex) { } public static Member of(String name, LocalDate birthDate, String email, Sex sex) { - return new Member(name, birthDate, email, sex); - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public String getEmail() { - return email; - } - - public Sex getSex() { - return sex; + Member member = new Member(); + member.name = name; + member.birthDate = birthDate; + member.email = email; + member.sex = sex; + return member; } } \ No newline at end of file diff --git a/src/main/java/org/sopt/dto/request/CommentCreateRequest.java b/src/main/java/org/sopt/dto/request/CommentCreateRequest.java new file mode 100644 index 0000000..8934c17 --- /dev/null +++ b/src/main/java/org/sopt/dto/request/CommentCreateRequest.java @@ -0,0 +1,16 @@ +package org.sopt.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record CommentCreateRequest( + @NotNull(message = "아티클 ID를 입력해주세요") + Long articleId, + + @NotNull(message = "작성자 ID를 입력해주세요") + Long memberId, + + @NotBlank(message = "내용을 입력해주세요") + String content +) { +} diff --git a/src/main/java/org/sopt/dto/request/CommentUpdateRequest.java b/src/main/java/org/sopt/dto/request/CommentUpdateRequest.java new file mode 100644 index 0000000..2e8c90d --- /dev/null +++ b/src/main/java/org/sopt/dto/request/CommentUpdateRequest.java @@ -0,0 +1,6 @@ +package org.sopt.dto.request; + +public record CommentUpdateRequest( + String content +) { +} diff --git a/src/main/java/org/sopt/dto/response/ArticleResponse.java b/src/main/java/org/sopt/dto/response/ArticleResponse.java index d2514f7..3d34638 100644 --- a/src/main/java/org/sopt/dto/response/ArticleResponse.java +++ b/src/main/java/org/sopt/dto/response/ArticleResponse.java @@ -4,6 +4,7 @@ import org.sopt.domain.Tag; import java.time.LocalDateTime; +import java.util.List; public record ArticleResponse( Long id, @@ -11,10 +12,13 @@ public record ArticleResponse( String title, String content, Tag tag, - LocalDateTime createdAt + LocalDateTime createdAt, + List comments ) { public record Author(Long memberId, String name) {} + public record Comment(Long commentId, Long memberId, String comment) {} + public static ArticleResponse from(Article article) { return new ArticleResponse( article.getId(), @@ -25,7 +29,14 @@ public static ArticleResponse from(Article article) { article.getTitle(), article.getContent(), article.getTag(), - article.getCreatedAt() + article.getCreatedAt(), + article.getComments().stream() + .map(comment -> new Comment( + comment.getId(), + comment.getMember().getId(), + comment.getContent() + )) + .toList() ); } } diff --git a/src/main/java/org/sopt/dto/response/CommentResponse.java b/src/main/java/org/sopt/dto/response/CommentResponse.java new file mode 100644 index 0000000..e0da55e --- /dev/null +++ b/src/main/java/org/sopt/dto/response/CommentResponse.java @@ -0,0 +1,35 @@ +package org.sopt.dto.response; + +import org.sopt.domain.Comment; + +import java.time.LocalDateTime; + +public record CommentResponse( + Long id, + Article article, + Writer writer, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public record Article(Long id, String title) {} + + public record Writer(Long id, String name) {} + + public static CommentResponse from(Comment comment) { + return new CommentResponse( + comment.getId(), + new Article( + comment.getArticle().getId(), + comment.getArticle().getTitle() + ), + new Writer( + comment.getMember().getId(), + comment.getMember().getName() + ), + comment.getContent(), + comment.getCreatedAt(), + comment.getUpdatedAt() + ); + } +} diff --git a/src/main/java/org/sopt/repository/CommentRepository.java b/src/main/java/org/sopt/repository/CommentRepository.java new file mode 100644 index 0000000..7c35901 --- /dev/null +++ b/src/main/java/org/sopt/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package org.sopt.repository; + +import org.sopt.domain.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/org/sopt/service/ArticleServiceImpl.java b/src/main/java/org/sopt/service/ArticleServiceImpl.java index 3af378a..f41a931 100644 --- a/src/main/java/org/sopt/service/ArticleServiceImpl.java +++ b/src/main/java/org/sopt/service/ArticleServiceImpl.java @@ -1,5 +1,6 @@ package org.sopt.service; +import lombok.RequiredArgsConstructor; import org.sopt.common.ErrorCode; import org.sopt.domain.Article; import org.sopt.domain.Member; @@ -14,16 +15,12 @@ import java.util.List; @Service +@RequiredArgsConstructor public class ArticleServiceImpl implements ArticleService { private final ArticleRepository articleRepository; private final MemberRepository memberRepository; - public ArticleServiceImpl(ArticleRepository articleRepository, MemberRepository memberRepository) { - this.articleRepository = articleRepository; - this.memberRepository = memberRepository; - } - // 아티클 생성 @Transactional public ArticleResponse create(Long memberId, String title, String content, Tag tag) { diff --git a/src/main/java/org/sopt/service/CommentService.java b/src/main/java/org/sopt/service/CommentService.java new file mode 100644 index 0000000..1296628 --- /dev/null +++ b/src/main/java/org/sopt/service/CommentService.java @@ -0,0 +1,14 @@ +package org.sopt.service; + +import org.sopt.dto.response.CommentResponse; + +public interface CommentService { + + Long create(Long articleId, Long memberId, String content); + + CommentResponse findById(Long id); + + void delete(Long commentId); + + CommentResponse update(Long commentId, String content); +} diff --git a/src/main/java/org/sopt/service/CommentServiceImpl.java b/src/main/java/org/sopt/service/CommentServiceImpl.java new file mode 100644 index 0000000..6300422 --- /dev/null +++ b/src/main/java/org/sopt/service/CommentServiceImpl.java @@ -0,0 +1,72 @@ +package org.sopt.service; + +import lombok.RequiredArgsConstructor; +import org.sopt.common.ErrorCode; +import org.sopt.domain.Article; +import org.sopt.domain.Comment; +import org.sopt.domain.Member; +import org.sopt.dto.response.CommentResponse; +import org.sopt.exception.BusinessException; +import org.sopt.repository.ArticleRepository; +import org.sopt.repository.CommentRepository; +import org.sopt.repository.MemberRepository; +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; + private final MemberRepository memberRepository; + private final ArticleRepository articleRepository; + + + // 댓글 생성 + @Transactional + public Long create(Long articleId, Long memberId, String content) { + Article article = articleRepository.findById(articleId).orElseThrow( + () -> new BusinessException(ErrorCode.ARTICLE_NOT_FOUND) + ); + + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND) + ); + + Comment comment = Comment.of(article, member, content); + + commentRepository.save(comment); + + return comment.getId(); + } + + // 댓글 조회 + public CommentResponse findById(Long commentId) { + Comment comment = commentRepository.findById(commentId).orElseThrow( + () -> new BusinessException(ErrorCode.COMMENT_NOT_FOUND) + ); + + return CommentResponse.from(comment); + } + + // 댓글 삭제 + @Transactional + public void delete(Long commentId) { + Comment comment = commentRepository.findById(commentId).orElseThrow( + () -> new BusinessException(ErrorCode.COMMENT_NOT_FOUND) + ); + commentRepository.delete(comment); + } + + // 댓글 수정 + @Transactional + public CommentResponse update(Long commentId, String content) { + Comment comment = commentRepository.findById(commentId).orElseThrow( + () -> new BusinessException(ErrorCode.COMMENT_NOT_FOUND) + ); + comment.update(content); + + return CommentResponse.from(comment); + } +} diff --git a/src/main/java/org/sopt/service/MemberServiceImpl.java b/src/main/java/org/sopt/service/MemberServiceImpl.java index 307e01b..8bf1ac3 100644 --- a/src/main/java/org/sopt/service/MemberServiceImpl.java +++ b/src/main/java/org/sopt/service/MemberServiceImpl.java @@ -1,5 +1,6 @@ package org.sopt.service; +import lombok.RequiredArgsConstructor; import org.sopt.common.ErrorCode; import org.sopt.domain.Member; import org.sopt.domain.Sex; @@ -15,13 +16,10 @@ import java.util.List; @Service +@RequiredArgsConstructor public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; - - public MemberServiceImpl(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } // 회원 추가 @Transactional @@ -60,23 +58,19 @@ public void delete(Long memberId) { // 회원 업데이트 @Transactional - public MemberResponse update(Long memberId, MemberUpdateRequest request) { - Member existingMember = memberRepository.findById(memberId) + public MemberResponse update(Long memberId, MemberUpdateRequest req) { + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); - if (!existingMember.getEmail().equals(request.email())) { - validateDuplicateEmail(request.email()); + if (!member.getEmail().equals(req.email())) { + validateDuplicateEmail(req.email()); } - validateAdult(request.birthDate()); + validateAdult(req.birthDate()); - Member updatedMember = Member.of( - request.name(), - request.birthDate(), - request.email(), - request.sex() - ); - return MemberResponse.from(updatedMember); + member.update(req.name(), req.birthDate(), req.email(), req.sex()); + + return MemberResponse.from(member); } // 이메일 중복체크