Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 47 additions & 0 deletions src/main/java/com/example/demo/controller/CommentController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.demo.controller;

import com.example.demo.domain.comment.dto.CommentAdoptRequest;
import com.example.demo.domain.comment.dto.CommentCreateRequest;
import com.example.demo.domain.comment.dto.CommentCreateResponse;
import com.example.demo.domain.comment.dto.CommentStatusResponse;
import com.example.demo.domain.comment.service.CommentService;
import com.example.demo.global.response.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class CommentController {

private final CommentService commentService;

@PostMapping("/api/posts/{postId}/comments")
public ResponseEntity<ApiResponse<CommentCreateResponse>> createComment(
@PathVariable Long postId,
@Valid @RequestBody CommentCreateRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(
"COMMENT_CREATE_SUCCESS",
"댓글 생성 성공",
commentService.createComment(postId, request)
));
}

@PostMapping("/api/comments/{commentId}/adoption")
Comment on lines +21 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

댓글 생성은 /api/posts/{postId}/comments처럼 “게시글의 하위 리소스인 댓글” 구조로 잘 잡혀 있는데, 댓글 채택 API는 /api/comments/{commentId}/adoption이라 리소스 기준이 달라 보여요.
댓글 채택도 특정 게시글 안의 특정 댓글에 대한 행동이므로, API 경로를 게시글 기준으로 맞추면 더 일관성 있어 보입니다.

만약 채택상태를 변경한다는 의미를 살리면 Patch 로 하는것 도 좋아보입니다!

@PatchMapping("/api/posts/{postId}/comments/{commentId}/adoption")

public ResponseEntity<ApiResponse<CommentStatusResponse>> adoptComment(
@PathVariable Long commentId,
@Valid @RequestBody CommentAdoptRequest request
) {
return ResponseEntity.ok(
ApiResponse.success(
"COMMENT_ADOPT_SUCCESS",
"댓글 채택 성공",
commentService.adoptComment(commentId, request.userId())
)
);
}
}
60 changes: 60 additions & 0 deletions src/main/java/com/example/demo/controller/ReportController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.example.demo.controller;

import com.example.demo.domain.report.dto.ReportCreateRequest;
import com.example.demo.domain.report.dto.ReportCreateResponse;
import com.example.demo.domain.report.dto.ReportResolveRequest;
import com.example.demo.domain.report.dto.ReportResolveResponse;
import com.example.demo.domain.report.service.ReportService;
import com.example.demo.global.response.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class ReportController {

private final ReportService reportService;

@PostMapping("/api/posts/{postId}/reports")
public ResponseEntity<ApiResponse<ReportCreateResponse>> reportPost(
@PathVariable Long postId,
@Valid @RequestBody ReportCreateRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(
"POST_REPORT_SUCCESS",
"게시글 신고 접수 성공",
reportService.reportPost(postId, request)
));
}

@PostMapping("/api/comments/{commentId}/reports")
public ResponseEntity<ApiResponse<ReportCreateResponse>> reportComment(
@PathVariable Long commentId,
@Valid @RequestBody ReportCreateRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(
"COMMENT_REPORT_SUCCESS",
"댓글 신고 접수 성공",
reportService.reportComment(commentId, request)
));
}

@PatchMapping("/api/reports/{reportId}/resolve")
public ResponseEntity<ApiResponse<ReportResolveResponse>> resolveReport(
@PathVariable Long reportId,
@Valid @RequestBody ReportResolveRequest request
) {
return ResponseEntity.ok(
ApiResponse.success(
"REPORT_RESOLVE_SUCCESS",
"신고 처리 완료",
reportService.resolveReport(reportId, request)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.demo.domain.comment.dto;

import jakarta.validation.constraints.NotNull;

public record CommentAdoptRequest(
@NotNull(message = "userId는 필수입니다.")
Long userId
Comment on lines +5 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필드값이 하나라면 (지금은) RequestParam으로 받는것도 방법이 될 수 있습니다!

참고로
추후에 인증/인가 주차에서 @AuthenticationPrincipal 또는 userdetail 을 이용하여 유저인증정보를 갖고 구성하면 좋을 것 같습니다!

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.demo.domain.comment.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record CommentCreateRequest(
@NotNull(message = "userId는 필수입니다.")
Long userId,

@NotBlank(message = "content는 필수입니다.")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.demo.domain.comment.dto;

import com.example.demo.domain.comment.entity.CommentStatus;

public record CommentCreateResponse(
Long commentId,
CommentStatus status
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.demo.domain.comment.dto;

import com.example.demo.domain.comment.entity.CommentStatus;

public record CommentStatusResponse(
Long commentId,
CommentStatus status
) {
}
25 changes: 23 additions & 2 deletions src/main/java/com/example/demo/domain/comment/entity/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,26 @@ public class Comment extends BaseEntity {
@Column(nullable = false, columnDefinition = "TEXT")
private String content;


}
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private CommentStatus status;

private Comment(User user, Post post, String content) {
this.user = user;
this.post = post;
this.content = content;
this.status = CommentStatus.ACTIVE;
}

public static Comment of(User user, Post post, String content) {
return new Comment(user, post, content);
}

public void hide() {
this.status = CommentStatus.HIDDEN;
}

public void adopt() {
this.status = CommentStatus.ADOPTED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.demo.domain.comment.entity;

public enum CommentStatus {
ACTIVE,
HIDDEN,
ADOPTED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.demo.domain.comment.repository;

import com.example.demo.domain.comment.entity.Comment;
import com.example.demo.domain.comment.entity.CommentStatus;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Long> {
boolean existsByPostIdAndStatus(Long postId, CommentStatus status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.example.demo.domain.comment.service;

import com.example.demo.domain.comment.dto.CommentCreateRequest;
import com.example.demo.domain.comment.dto.CommentCreateResponse;
import com.example.demo.domain.comment.dto.CommentStatusResponse;
import com.example.demo.domain.comment.entity.Comment;
import com.example.demo.domain.comment.entity.CommentStatus;
import com.example.demo.domain.comment.repository.CommentRepository;
import com.example.demo.domain.post.entity.Post;
import com.example.demo.domain.post.entity.PostStatus;
import com.example.demo.domain.post.repository.PostRepository;
import com.example.demo.domain.user.entity.User;
import com.example.demo.domain.user.repository.UserRepository;
import com.example.demo.global.exception.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

private final CommentRepository commentRepository;
private final PostRepository postRepository;
private final UserRepository userRepository;

@Transactional
public CommentCreateResponse createComment(Long postId, CommentCreateRequest request) {
Post post = findPost(postId);
User user = findUser(request.userId());

if (post.getStatus() == PostStatus.HIDDEN) {
throw new CustomException(HttpStatus.BAD_REQUEST, "POST_HIDDEN", "숨김 처리된 게시글에는 댓글을 작성할 수 없습니다.");
}

Comment comment = commentRepository.save(Comment.of(user, post, request.content()));
return new CommentCreateResponse(comment.getId(), comment.getStatus());
}

@Transactional
public CommentStatusResponse adoptComment(Long commentId, Long userId) {
Comment comment = findComment(commentId);
Post post = comment.getPost();

if (!post.getUser().getId().equals(userId)) {
throw new CustomException(HttpStatus.FORBIDDEN, "FORBIDDEN", "댓글 채택 권한이 없습니다.");
}
if (post.getStatus() == PostStatus.HIDDEN) {
throw new CustomException(HttpStatus.BAD_REQUEST, "POST_HIDDEN", "숨김 처리된 게시글에서는 댓글을 채택할 수 없습니다.");
}
if (comment.getStatus() == CommentStatus.HIDDEN) {
throw new CustomException(HttpStatus.BAD_REQUEST, "COMMENT_HIDDEN", "숨김 처리된 댓글은 채택할 수 없습니다.");
}
if (commentRepository.existsByPostIdAndStatus(post.getId(), CommentStatus.ADOPTED)) {
throw new CustomException(HttpStatus.CONFLICT, "COMMENT_ALREADY_ADOPTED", "이미 채택된 댓글이 있습니다.");
}

comment.adopt();
return new CommentStatusResponse(comment.getId(), comment.getStatus());
}

public Comment findComment(Long commentId) {
return commentRepository.findById(commentId)
.orElseThrow(() -> new CustomException(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "해당 댓글을 찾을 수 없습니다."));
}

private Post findPost(Long postId) {
return postRepository.findById(postId)
.orElseThrow(() -> new CustomException(HttpStatus.NOT_FOUND, "POST_NOT_FOUND", "해당 게시글을 찾을 수 없습니다."));
}

private User findUser(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new CustomException(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "해당 사용자를 찾을 수 없습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.example.demo.domain.post.dto;

import com.example.demo.domain.post.entity.PostStatus;

import java.time.LocalDateTime;

public record PostDetailResponse(
Long postId,
String title,
String content,
String imageUrl,
PostStatus status,
Long authorId,
String author,
LocalDateTime createdAt,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.example.demo.domain.post.dto;

import com.example.demo.domain.post.entity.PostStatus;

import java.time.LocalDateTime;

public record PostListItemResponse(
Long postId,
String title,
String content,
String thumbnailImageUrl,
PostStatus status,
String author,
LocalDateTime createdAt
) {
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/com/example/demo/domain/post/entity/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public class Post extends BaseEntity {
@Column(name = "image_url")
private String imageUrl;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PostStatus status;

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();

Expand All @@ -46,6 +50,7 @@ private Post(User user, String title, String content, String imageUrl) {
this.title = title;
this.content = content;
this.imageUrl = imageUrl;
this.status = PostStatus.ACTIVE;
}

public static Post of(User user, String title, String content, String imageUrl) {
Expand All @@ -61,4 +66,8 @@ public void update(String title, String content, String imageUrl) {
}
this.imageUrl = imageUrl;
}
}

public void hide() {
this.status = PostStatus.HIDDEN;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.demo.domain.post.entity;

public enum PostStatus {
ACTIVE,
HIDDEN
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public PostListResponse getPosts(int page, int size) {
post.getTitle(),
post.getContent(),
post.getImageUrl(),
post.getStatus(),
post.getUser().getName(),
post.getCreatedAt()
))
Expand All @@ -53,6 +54,7 @@ public PostDetailResponse getPost(Long postId) {
post.getTitle(),
post.getContent(),
post.getImageUrl(),
post.getStatus(),
post.getUser().getId(),
post.getUser().getName(),
post.getCreatedAt(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.demo.domain.report.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record ReportCreateRequest(
@NotNull(message = "reporterId는 필수입니다.")
Long reporterId,

@NotBlank(message = "reason은 필수입니다.")
String reason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.demo.domain.report.dto;

import com.example.demo.domain.report.entity.ReportStatus;
import com.example.demo.domain.report.entity.ReportTargetType;

public record ReportCreateResponse(
Long reportId,
ReportTargetType targetType,
ReportStatus status
) {
}
Loading