Skip to content

Commit 3138b54

Browse files
Merge pull request #173 from prgrms-web-devcourse-final-project/feat/#156
[Community] 커뮤니티 포스트, 좋아요, 댓글 구현
2 parents 5596fb6 + c18de8a commit 3138b54

22 files changed

Lines changed: 950 additions & 0 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.controller;
2+
3+
import com.back.web7_9_codecrete_be.domain.community.comment.dto.request.CommentCreateRequest;
4+
import com.back.web7_9_codecrete_be.domain.community.comment.service.CommentService;
5+
import com.back.web7_9_codecrete_be.domain.users.entity.User;
6+
import com.back.web7_9_codecrete_be.global.rq.Rq;
7+
import com.back.web7_9_codecrete_be.global.rsData.RsData;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
@RestController
15+
@RequestMapping("/api/v1/posts/{postId}/comments")
16+
@RequiredArgsConstructor
17+
@Tag(name = "Community - Comment", description = "커뮤니티 댓글 API")
18+
public class CommentController {
19+
20+
private final CommentService commentService;
21+
private final Rq rq;
22+
23+
@Operation(summary = "댓글 작성", description = "특정 게시글에 댓글을 작성합니다.")
24+
@PostMapping
25+
public RsData<?> createComment(
26+
@PathVariable Long postId,
27+
@Valid @RequestBody CommentCreateRequest req
28+
) {
29+
User user = rq.getUser();
30+
Long commentId = commentService.create(postId, req, user.getId());
31+
return RsData.success("댓글이 작성되었습니다.", commentId);
32+
}
33+
34+
@GetMapping
35+
@Operation(summary = "댓글 목록 조회", description = "게시글 댓글을 페이지 단위로 조회합니다.")
36+
public RsData<?> getComments(
37+
@PathVariable Long postId,
38+
@RequestParam(defaultValue = "1") int page
39+
) {
40+
return RsData.success("댓글 목록 조회 성공", commentService.getComments(postId, page));
41+
}
42+
43+
@Operation(summary = "댓글 삭제", description = "작성자 본인만 댓글을 삭제할 수 있습니다.")
44+
@DeleteMapping("/{commentId}")
45+
public RsData<?> deleteComment(
46+
@PathVariable Long postId,
47+
@PathVariable Long commentId
48+
) {
49+
User user = rq.getUser();
50+
commentService.delete(commentId, user.getId());
51+
return RsData.success("댓글이 삭제되었습니다.");
52+
}
53+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Schema(description = "댓글 작성 요청 DTO")
9+
public class CommentCreateRequest {
10+
11+
@NotBlank(message = "댓글 내용은 필수입니다.")
12+
@Schema(description = "댓글 내용", example = "저도 이 공연 다녀왔어요!")
13+
private String content;
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.dto.response;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import org.springframework.data.domain.Page;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
@Builder
11+
public class CommentPageResponse<T> {
12+
13+
private List<T> content; // 댓글 목록
14+
private int page; // 현재 페이지 (1-based)
15+
private int size; // 페이지 크기
16+
private int totalPages; // 전체 페이지 수
17+
private long totalElements; // 전체 댓글 수
18+
private boolean hasNext; // 다음 페이지 존재 여부
19+
20+
public static <T> CommentPageResponse<T> from(Page<T> pageResult) {
21+
return CommentPageResponse.<T>builder()
22+
.content(pageResult.getContent())
23+
.page(pageResult.getNumber() + 1) // 0-based → 1-based
24+
.size(pageResult.getSize())
25+
.totalPages(pageResult.getTotalPages())
26+
.totalElements(pageResult.getTotalElements())
27+
.hasNext(pageResult.hasNext())
28+
.build();
29+
}
30+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.dto.response;
2+
3+
import com.back.web7_9_codecrete_be.domain.community.comment.entity.Comment;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Getter
11+
@Builder
12+
@Schema(description = "댓글 응답 DTO")
13+
public class CommentResponse {
14+
15+
@Schema(description = "댓글 ID", example = "5")
16+
private Long commentId;
17+
18+
@Schema(description = "작성자 사용자 ID", example = "12")
19+
private Long userId;
20+
21+
@Schema(description = "댓글 내용", example = "정말 공감합니다!")
22+
private String content;
23+
24+
@Schema(description = "작성일시", example = "2025-01-03T18:40:00")
25+
private LocalDateTime createdDate;
26+
27+
public static CommentResponse from(Comment comment) {
28+
return CommentResponse.builder()
29+
.commentId(comment.getCommentId())
30+
.userId(comment.getUserId())
31+
.content(comment.getContent())
32+
.createdDate(comment.getCreatedDate())
33+
.build();
34+
}
35+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.entity;
2+
3+
import com.back.web7_9_codecrete_be.domain.community.post.entity.Post;
4+
import jakarta.persistence.*;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import org.hibernate.annotations.CreationTimestamp;
9+
import org.hibernate.annotations.UpdateTimestamp;
10+
11+
import java.time.LocalDateTime;
12+
13+
@Getter
14+
@Entity
15+
@NoArgsConstructor
16+
@Table(name = "comment")
17+
public class Comment {
18+
19+
@Id
20+
@GeneratedValue(strategy = GenerationType.IDENTITY)
21+
@Column(name = "comment_id")
22+
private Long commentId;
23+
24+
@ManyToOne(fetch = FetchType.LAZY)
25+
@JoinColumn(name = "post_id", nullable = false)
26+
private Post post;
27+
28+
@Column(name = "user_id", nullable = false)
29+
private Long userId;
30+
31+
@Column(nullable = false, length = 500)
32+
private String content;
33+
34+
@CreationTimestamp
35+
@Column(name = "created_date", nullable = false, updatable = false)
36+
private LocalDateTime createdDate;
37+
38+
@UpdateTimestamp
39+
@Column(name = "modified_date", nullable = false)
40+
private LocalDateTime modifiedDate;
41+
42+
@Builder
43+
private Comment(Post post, Long userId, String content) {
44+
this.post = post;
45+
this.userId = userId;
46+
this.content = content;
47+
}
48+
49+
// 생성
50+
public static Comment create(Post post, Long userId, String content) {
51+
return Comment.builder()
52+
.post(post)
53+
.userId(userId)
54+
.content(content)
55+
.build();
56+
}
57+
58+
// 수정
59+
public void update(String content) {
60+
this.content = content;
61+
}
62+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.repository;
2+
3+
import com.back.web7_9_codecrete_be.domain.community.comment.entity.Comment;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
public interface CommentRepository extends JpaRepository<Comment, Long> {
9+
Page<Comment> findByPost_PostId(Long postId, Pageable pageable);
10+
}
11+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.back.web7_9_codecrete_be.domain.community.comment.service;
2+
3+
import com.back.web7_9_codecrete_be.domain.community.comment.dto.request.CommentCreateRequest;
4+
import com.back.web7_9_codecrete_be.domain.community.comment.dto.response.CommentPageResponse;
5+
import com.back.web7_9_codecrete_be.domain.community.comment.dto.response.CommentResponse;
6+
import com.back.web7_9_codecrete_be.domain.community.comment.entity.Comment;
7+
import com.back.web7_9_codecrete_be.domain.community.comment.repository.CommentRepository;
8+
import com.back.web7_9_codecrete_be.domain.community.post.entity.Post;
9+
import com.back.web7_9_codecrete_be.domain.community.post.repository.PostRepository;
10+
import com.back.web7_9_codecrete_be.global.error.code.CommentErrorCode;
11+
import com.back.web7_9_codecrete_be.global.error.code.PostErrorCode;
12+
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.data.domain.Page;
15+
import org.springframework.data.domain.PageRequest;
16+
import org.springframework.data.domain.Pageable;
17+
import org.springframework.data.domain.Sort;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
@Service
22+
@RequiredArgsConstructor
23+
@Transactional(readOnly = true)
24+
public class CommentService {
25+
26+
private final CommentRepository commentRepository;
27+
private final PostRepository postRepository;
28+
29+
// 댓글 생성
30+
@Transactional
31+
public Long create(Long postId, CommentCreateRequest req, Long userId) {
32+
Post post = postRepository.findById(postId)
33+
.orElseThrow(() -> new BusinessException(PostErrorCode.POST_NOT_FOUND));
34+
35+
Comment comment = Comment.create(
36+
post,
37+
userId,
38+
req.getContent()
39+
);
40+
41+
return commentRepository.save(comment).getCommentId();
42+
}
43+
44+
// 댓글 조회
45+
public CommentPageResponse<CommentResponse> getComments(Long postId, int page) {
46+
47+
Pageable pageable = PageRequest.of(
48+
page - 1,
49+
20,
50+
Sort.by(Sort.Direction.ASC, "createdDate")
51+
);
52+
53+
Page<CommentResponse> result =
54+
commentRepository.findByPost_PostId(postId, pageable)
55+
.map(CommentResponse::from);
56+
57+
return CommentPageResponse.from(result);
58+
}
59+
60+
// 댓글 삭제
61+
@Transactional
62+
public void delete(Long commentId, Long userId) {
63+
Comment comment = commentRepository.findById(commentId)
64+
.orElseThrow(() -> new BusinessException(CommentErrorCode.COMMENT_NOT_FOUND));
65+
66+
if (!comment.getUserId().equals(userId)) {
67+
throw new BusinessException(CommentErrorCode.NO_COMMENT_PERMISSION);
68+
}
69+
70+
commentRepository.delete(comment);
71+
}
72+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.back.web7_9_codecrete_be.domain.community.post.controller;
2+
3+
import com.back.web7_9_codecrete_be.domain.community.post.dto.request.PostCreateRequest;
4+
import com.back.web7_9_codecrete_be.domain.community.post.dto.request.PostUpdateRequest;
5+
import com.back.web7_9_codecrete_be.domain.community.post.dto.response.PostPageResponse;
6+
import com.back.web7_9_codecrete_be.domain.community.post.dto.response.PostResponse;
7+
import com.back.web7_9_codecrete_be.domain.community.post.entity.PostCategory;
8+
import com.back.web7_9_codecrete_be.domain.community.post.service.PostService;
9+
import com.back.web7_9_codecrete_be.domain.users.entity.User;
10+
import com.back.web7_9_codecrete_be.global.rq.Rq;
11+
import com.back.web7_9_codecrete_be.global.rsData.RsData;
12+
import io.swagger.v3.oas.annotations.Operation;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import jakarta.validation.Valid;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.web.bind.annotation.*;
17+
18+
@RestController
19+
@RequestMapping("/api/v1/posts")
20+
@RequiredArgsConstructor
21+
@Tag(name = "Community - Post", description = "커뮤니티 게시글 API")
22+
public class PostController {
23+
24+
private final PostService postService;
25+
private final Rq rq;
26+
27+
@Operation(summary = "게시글 작성", description = "카테고리, 제목, 내용을 입력하여 게시글을 작성합니다.")
28+
@PostMapping
29+
public RsData<?> createPost(@Valid @RequestBody PostCreateRequest req) {
30+
User user = rq.getUser();
31+
Long postId = postService.create(req, user);
32+
return RsData.success("게시글이 작성되었습니다.", postId);
33+
}
34+
35+
@Operation(summary = "게시글 단건 조회", description = "게시글 ID로 게시글 상세 정보를 조회합니다.")
36+
@GetMapping("/{postId}")
37+
public RsData<?> getPost(@PathVariable Long postId) {
38+
PostResponse response = postService.getPost(postId);
39+
return RsData.success("게시글 조회 성공", response);
40+
}
41+
42+
@GetMapping
43+
@Operation(summary = "게시글 목록 조회", description = "커뮤니티 게시글을 페이지 단위로 조회합니다.")
44+
public RsData<PostPageResponse<PostResponse>> getPosts(
45+
@RequestParam(defaultValue = "1") int page
46+
) {
47+
return RsData.success("게시글 목록 조회 성공", postService.getPosts(page));
48+
}
49+
50+
@GetMapping("/category/{category}")
51+
@Operation(
52+
summary = "카테고리별 게시글 조회",
53+
description = "특정 카테고리에 속한 게시글을 페이지 단위로 조회합니다."
54+
)
55+
public RsData<PostPageResponse<PostResponse>> getPostsByCategory(
56+
@PathVariable PostCategory category,
57+
@RequestParam(defaultValue = "1") int page
58+
) {
59+
return RsData.success("카테고리별 게시글 조회 성공", postService.getPostsByCategory(page, category));
60+
}
61+
62+
@Operation(summary = "게시글 수정", description = "작성자 본인만 게시글을 수정할 수 있습니다.")
63+
@PutMapping("/{postId}")
64+
public RsData<?> updatePost(
65+
@PathVariable Long postId,
66+
@Valid @RequestBody PostUpdateRequest req
67+
) {
68+
User user = rq.getUser();
69+
postService.update(postId, req, user.getId());
70+
return RsData.success("게시글이 수정되었습니다.");
71+
}
72+
73+
@Operation(summary = "게시글 삭제", description = "작성자 본인만 게시글을 삭제할 수 있습니다.")
74+
@DeleteMapping("/{postId}")
75+
public RsData<?> deletePost(@PathVariable Long postId) {
76+
User user = rq.getUser();
77+
postService.delete(postId, user.getId());
78+
return RsData.success("게시글이 삭제되었습니다.");
79+
}
80+
}

0 commit comments

Comments
 (0)