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);
}
// 이메일 중복체크