From 9b778f5c808e52b02469d925dcd8119c7057661c Mon Sep 17 00:00:00 2001 From: Kunhee Lee Date: Mon, 27 Apr 2026 20:00:01 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EB=B0=8F=20=EC=88=A8?= =?UTF-8?q?=EA=B9=80=20API=20=EA=B5=AC=ED=98=84.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostController.java | 9 +++++ .../assignment/domain/post/entity/Post.java | 18 ++++++++++ .../domain/post/entity/PostStatus.java | 14 ++++++++ .../domain/post/service/PostService.java | 33 +++++++++++++++++-- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/PostStatus.java diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/controller/PostController.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/controller/PostController.java index 90f59f94..56a729ab 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/controller/PostController.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/controller/PostController.java @@ -65,4 +65,13 @@ public ApiResponse deletePost( return ApiResponse.onSuccess("POST200_4", "게시글 삭제에 성공했습니다.", null); } + // 6. 게시글 숨기기 + @PatchMapping("/{postId}/hide") + public ApiResponse hidePost( + @PathVariable Long postId, + @RequestParam Long userId // 실제로는 인증된 유저 정보를 사용해야 함 + ) { + postService.hidePost(postId, userId); + return ApiResponse.onSuccess("POST200_5", "게시글이 숨김 처리되었습니다.", null); + } } \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/Post.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/Post.java index 822c93d3..1915f9b8 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/Post.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/Post.java @@ -23,6 +23,10 @@ public class Post extends BaseEntity { @Column(name = "title", nullable = false) private String title; + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PostStatus status = PostStatus.ACTIVE; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") // FK 컬럼명 명시 private User user; @@ -34,6 +38,20 @@ public class Post extends BaseEntity { private Post(String title, User user) { this.title = title; this.user = user; + this.status = PostStatus.ACTIVE; + } + + + public void hideByUser() { + this.status = PostStatus.HIDDEN_BY_USER; + } + + public void hideByAdmin() { + this.status = PostStatus.HIDDEN_BY_ADMIN; + } + + public void unhide() { + this.status = PostStatus.ACTIVE; } public void softDelete() { diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/PostStatus.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/PostStatus.java new file mode 100644 index 00000000..c3a74783 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/entity/PostStatus.java @@ -0,0 +1,14 @@ +package com.leets.assignment.domain.post.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PostStatus { + ACTIVE("Active"), + HIDDEN_BY_USER("Hidden by user"), + HIDDEN_BY_ADMIN("Hidden by admin"); + + private final String description; +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java index 555297ab..4019f859 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java @@ -4,6 +4,7 @@ import com.leets.assignment.domain.post.dto.res.PostResponseDTO; import com.leets.assignment.domain.post.entity.Post; import com.leets.assignment.domain.post.entity.PostBlock; +import com.leets.assignment.domain.post.entity.PostStatus; import com.leets.assignment.domain.post.exception.code.PostErrorCode; import com.leets.assignment.domain.post.exception.PostException; import com.leets.assignment.domain.post.repository.PostRepository; @@ -62,6 +63,7 @@ public PostResponseDTO.PostDetailResDTO getPost(Long postId) { // 2. 데이터가 없으면 PostNotFoundException 예외 발생! (-> 404 응답) Post post = postRepository.findById(postId) .filter(p -> p.getDeletedAt() == null) // 삭제 안 된 것만 필터링 + .filter(p -> p.getStatus() == PostStatus.ACTIVE) // 활성 상태 필터링 .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND)); // 3. 찾은 엔티티를 DTO로 변환하여 반환 @@ -73,6 +75,7 @@ public List getPostList() { // DB의 모든 글을 가져와서 ListResDTO로 변환 return postRepository.findAll().stream() .filter(post -> post.getDeletedAt() == null) // 삭제된 글 제외 + .filter(post -> post.getStatus() == PostStatus.ACTIVE) // 활성 상태 필터링 .map(PostResponseDTO.PostListResDTO::from) .collect(Collectors.toList()); } @@ -102,15 +105,20 @@ public PostResponseDTO.PostDetailResDTO updatePost(Long postId, PostRequestDTO.U .filter(p -> p.getDeletedAt() == null) .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND)); - // 2. 수정 권한 확인 + // 2. 관리자가 숨긴 글은 수정 불가 + if (post.getStatus() == PostStatus.HIDDEN_BY_ADMIN) { + throw new PostException(PostErrorCode.POST_FORBIDDEN); // 혹은 전용 에러 코드 사용 + } + + // 3. 수정 권한 확인 if (!post.getUser().getUserId().equals(request.getUserId())) { throw new PostException(PostErrorCode.POST_FORBIDDEN); } - // 3. 제목 수정 (Dirty Checking) + // 4. 제목 수정 (Dirty Checking) post.update(request.getTitle()); - // 4. 블록 수정 (기존 블록 비우고 새로 추가) + // 5. 블록 수정 (기존 블록 비우고 새로 추가) post.getBlocks().clear(); request.getBlocks().forEach(blockDto -> { PostBlock block = PostBlock.builder() @@ -124,4 +132,23 @@ public PostResponseDTO.PostDetailResDTO updatePost(Long postId, PostRequestDTO.U return PostResponseDTO.PostDetailResDTO.from(post); } + + // 게시글 숨김 + @Transactional + public void hidePost(Long postId, Long userId) { + // 1. 게시글 존재 및 삭제 여부 확인 + Post post = postRepository.findById(postId) + .filter(p -> p.getDeletedAt() == null) + .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND)); + + // 2. 권한 확인 (관리자 기능이 없다면 우선 작성자 혹은 특정 조건 확인) + if (!post.getUser().getUserId().equals(userId)) { + throw new PostException(PostErrorCode.POST_FORBIDDEN); + } + + // 3. 상태 변경 (ACTIVE -> HIDDEN) + post.hideByUser(); + } + + } \ No newline at end of file From 3d0b1d71dcbc35999cad8acab5ed4d0fcd97e1ec Mon Sep 17 00:00:00 2001 From: Kunhee Lee Date: Mon, 27 Apr 2026 22:54:26 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Feat:=20=EC=8B=A0=EA=B3=A0=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20BaseReport=20=EB=B0=8F=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=8B=A0=EA=B3=A0=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/report/entity/BaseReport.java | 39 +++++++++++++++++++ .../domain/report/entity/PostReport.java | 26 +++++++++++++ .../domain/report/entity/ReportStatus.java | 13 +++++++ 3 files changed, 78 insertions(+) create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/BaseReport.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/PostReport.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/ReportStatus.java diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/BaseReport.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/BaseReport.java new file mode 100644 index 00000000..1957dc53 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/BaseReport.java @@ -0,0 +1,39 @@ +package com.leets.assignment.domain.report.entity; + +import com.leets.assignment.domain.user.entity.User; +import com.leets.assignment.global.baseEntity.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@MappedSuperclass +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class BaseReport extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long reportId; // 신고 아이디 + + @Column(nullable = false) + private String reason; // 신고 사유 + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReportStatus status = ReportStatus.PENDING; // 신고 상태 (기본값: 대기중) + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reporter_id", nullable = false) + private User reporter; // 신고자 + + protected BaseReport(User reporter, String reason) { + this.reporter = reporter; + this.reason = reason; + this.status = ReportStatus.PENDING; + } + + public void resolve() { + this.status = ReportStatus.RESOLVED; + } +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/PostReport.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/PostReport.java new file mode 100644 index 00000000..6cb41422 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/PostReport.java @@ -0,0 +1,26 @@ +package com.leets.assignment.domain.report.entity; + +import com.leets.assignment.domain.post.entity.Post; +import com.leets.assignment.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "post_reports") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostReport extends BaseReport { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; // 신고 대상 게시글 + + @Builder + public PostReport(User reporter, Post post, String reason) { + super(reporter, reason); + this.post = post; + } +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/ReportStatus.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/ReportStatus.java new file mode 100644 index 00000000..e8263414 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/entity/ReportStatus.java @@ -0,0 +1,13 @@ +package com.leets.assignment.domain.report.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReportStatus { + PENDING("Pending"), // 대기 중 + RESOLVED("Resolved"); // 처리 완료 + + private final String description; +} \ No newline at end of file From cd58b6c7bea8c98fc62b3af91b9b731093c63c01 Mon Sep 17 00:00:00 2001 From: Kunhee Lee Date: Tue, 28 Apr 2026 17:13:24 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20API=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/service/PostService.java | 4 +- .../controller/PostReportController.java | 26 +++++++ .../report/dto/req/PostReportRequestDTO.java | 13 ++++ .../report/dto/res/PostReportResponseDTO.java | 16 +++++ .../report/exception/ReportException.java | 14 ++++ .../exception/code/ReportErrorCode.java | 18 +++++ .../repository/PostReportRepository.java | 11 +++ .../report/service/PostReportService.java | 67 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 16 +++++ LeeKunHee/src/main/resources/data.sql | 24 ++++++- 10 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/req/PostReportRequestDTO.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/res/PostReportResponseDTO.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/ReportException.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java create mode 100644 LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java index 4019f859..9da00570 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/post/service/PostService.java @@ -136,13 +136,15 @@ public PostResponseDTO.PostDetailResDTO updatePost(Long postId, PostRequestDTO.U // 게시글 숨김 @Transactional public void hidePost(Long postId, Long userId) { + // 1. 게시글 존재 및 삭제 여부 확인 Post post = postRepository.findById(postId) .filter(p -> p.getDeletedAt() == null) .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND)); // 2. 권한 확인 (관리자 기능이 없다면 우선 작성자 혹은 특정 조건 확인) - if (!post.getUser().getUserId().equals(userId)) { + // post.getUser()가 null인지 먼저 확인하거나, equals를 활용 + if (post.getUser() == null || !post.getUser().getUserId().equals(userId)) { throw new PostException(PostErrorCode.POST_FORBIDDEN); } diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java new file mode 100644 index 00000000..8c069f5a --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java @@ -0,0 +1,26 @@ +package com.leets.assignment.domain.report.controller; + +import com.leets.assignment.domain.report.dto.req.PostReportRequestDTO; +import com.leets.assignment.domain.report.dto.res.PostReportResponseDTO; +import com.leets.assignment.domain.report.service.PostReportService; +import com.leets.assignment.global.common.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +public class PostReportController { + + private final PostReportService postReportService; + + @PostMapping("/{postId}/reports") + public ApiResponse createReport( + @PathVariable Long postId, + @Valid @RequestBody PostReportRequestDTO request) { + + PostReportResponseDTO response = postReportService.reportPost(postId, request.reporterId(), request.reason()); + return ApiResponse.onSuccess("REPORT201", "신고가 정상적으로 접수되었습니다.", response); + } +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/req/PostReportRequestDTO.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/req/PostReportRequestDTO.java new file mode 100644 index 00000000..f0337642 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/req/PostReportRequestDTO.java @@ -0,0 +1,13 @@ +package com.leets.assignment.domain.report.dto.req; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record PostReportRequestDTO( + @NotNull(message = "신고자 ID는 필수입니다.") + Long reporterId, + + @NotBlank(message = "신고 사유를 입력해주세요.") + String reason +) { +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/res/PostReportResponseDTO.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/res/PostReportResponseDTO.java new file mode 100644 index 00000000..9ec2cd45 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/dto/res/PostReportResponseDTO.java @@ -0,0 +1,16 @@ +package com.leets.assignment.domain.report.dto.res; + +import com.leets.assignment.domain.report.entity.ReportStatus; +import lombok.Builder; +import java.time.LocalDateTime; + +@Builder +public record PostReportResponseDTO( + Long reportId, + Long postId, + Long reporterId, + String reason, + ReportStatus status, + LocalDateTime createdAt +) { +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/ReportException.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/ReportException.java new file mode 100644 index 00000000..302c4e37 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/ReportException.java @@ -0,0 +1,14 @@ +package com.leets.assignment.domain.report.exception; + +import com.leets.assignment.domain.report.exception.code.ReportErrorCode; +import lombok.Getter; + +@Getter +public class ReportException extends RuntimeException { + private final ReportErrorCode errorCode; + + public ReportException(ReportErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java new file mode 100644 index 00000000..4eeb34c4 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java @@ -0,0 +1,18 @@ +package com.leets.assignment.domain.report.exception.code; + +import com.leets.assignment.global.exception.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReportErrorCode implements BaseErrorCode { + REPORT_DUPLICATED("REPORT409_1", "이미 신고한 게시글입니다.", HttpStatus.CONFLICT), + REPORT_SELF_FORBIDDEN("REPORT403_1", "자신의 게시글은 신고할 수 없습니다.", HttpStatus.FORBIDDEN), + REPORT_NOT_FOUND("REPORT404_1", "해당 신고 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND); + + private final String code; + private final String message; + private final HttpStatus httpStatus; +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java new file mode 100644 index 00000000..814f1e45 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java @@ -0,0 +1,11 @@ +package com.leets.assignment.domain.report.repository; + +import com.leets.assignment.domain.post.entity.Post; +import com.leets.assignment.domain.report.entity.PostReport; +import com.leets.assignment.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostReportRepository extends JpaRepository { + // 중복 신고 확인: 동일 신고자가 동일 게시글을 이미 신고했는지 체크 + boolean existsByReporter_UserIdAndPost_PostId(Long userId, Long postId); +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java new file mode 100644 index 00000000..fd2d6822 --- /dev/null +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java @@ -0,0 +1,67 @@ +package com.leets.assignment.domain.report.service; + +import com.leets.assignment.domain.post.entity.Post; +import com.leets.assignment.domain.post.exception.PostException; +import com.leets.assignment.domain.post.exception.code.PostErrorCode; +import com.leets.assignment.domain.post.repository.PostRepository; +import com.leets.assignment.domain.report.dto.res.PostReportResponseDTO; +import com.leets.assignment.domain.report.entity.PostReport; +import com.leets.assignment.domain.report.exception.ReportException; +import com.leets.assignment.domain.report.exception.code.ReportErrorCode; +import com.leets.assignment.domain.report.repository.PostReportRepository; +import com.leets.assignment.domain.user.entity.User; +import com.leets.assignment.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PostReportService { + + private final PostReportRepository postReportRepository; + private final PostRepository postRepository; + private final UserRepository userRepository; + + @Transactional + public PostReportResponseDTO reportPost(Long postId, Long reporterId, String reason) { + // 1. 엔티티 조회 + // 유저가 없는 경우는 일반 런타임 예외나 별도의 UserException을 던질 수 있습니다. + User reporter = userRepository.findById(reporterId) + .orElseThrow(() -> new RuntimeException("존재하지 않는 유저입니다.")); + + // 게시글이 없는 경우 PostErrorCode 사용 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND)); + + // 2. 권한 및 중복 체크 (Report 전용 예외 사용) + // 본인 글 신고 불가 + if (post.getUser().getUserId().equals(reporterId)) { + throw new ReportException(ReportErrorCode.REPORT_SELF_FORBIDDEN); + } + + // 중복 신고 체크 + if (postReportRepository.existsByReporter_UserIdAndPost_PostId(reporterId, postId)) { + throw new ReportException(ReportErrorCode.REPORT_DUPLICATED); + } + + // 3. 신고 엔티티 생성 및 저장 + PostReport report = PostReport.builder() + .reporter(reporter) + .post(post) + .reason(reason) + .build(); + + PostReport savedReport = postReportRepository.save(report); + + // 4. DTO 변환 및 반환 + return PostReportResponseDTO.builder() + .reportId(savedReport.getReportId()) + .postId(savedReport.getPost().getPostId()) + .reporterId(savedReport.getReporter().getUserId()) + .reason(savedReport.getReason()) + .status(savedReport.getStatus()) + .createdAt(savedReport.getCreatedAt()) + .build(); + } +} \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/global/exception/GlobalExceptionHandler.java b/LeeKunHee/src/main/java/com/leets/assignment/global/exception/GlobalExceptionHandler.java index 0a27acbb..29c217da 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/global/exception/GlobalExceptionHandler.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/global/exception/GlobalExceptionHandler.java @@ -2,6 +2,8 @@ import com.leets.assignment.domain.post.exception.code.PostErrorCode; import com.leets.assignment.domain.post.exception.PostException; +import com.leets.assignment.domain.report.exception.ReportException; +import com.leets.assignment.domain.report.exception.code.ReportErrorCode; import com.leets.assignment.global.common.ApiResponse; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -69,6 +71,20 @@ public ResponseEntity> handleHttpMessageNotReadableException(H ); } + /** + * [REPORT400] ReportException + */ + @ExceptionHandler(ReportException.class) + public ResponseEntity> handleReportException(ReportException e) { + ReportErrorCode errorCode = e.getErrorCode(); + return ResponseEntity.status(errorCode.getHttpStatus()).body( + ApiResponse.builder() + .isSuccess(false) + .code(errorCode.getCode()) + .message(errorCode.getMessage()) + .build() + ); + } /** * [COMMON500_1] 기타 예기치 못한 서버 에러 diff --git a/LeeKunHee/src/main/resources/data.sql b/LeeKunHee/src/main/resources/data.sql index 85992066..86d676b1 100644 --- a/LeeKunHee/src/main/resources/data.sql +++ b/LeeKunHee/src/main/resources/data.sql @@ -1,3 +1,23 @@ --- 테스트용 유저 데이터 (서버 실행 시 자동 삽입) +-- 1번 유저 INSERT INTO users (user_id, email, name, nickname, password, created_at, updated_at) -VALUES (1, 'test@test.com', '강아지', '가나디', '1234', NOW(), NOW()); \ No newline at end of file +VALUES (1, 'test1@test.com', '강아지1', '가나디1', '1234', NOW(), NOW()); + +-- 2번 유저 +INSERT INTO users (user_id, email, name, nickname, password, created_at, updated_at) +VALUES (2, 'test2@test.com', '강아지2', '가나디2', '1234', NOW(), NOW()); + +-- 3번 유저 +INSERT INTO users (user_id, email, name, nickname, password, created_at, updated_at) +VALUES (3, 'test3@test.com', '강아지3', '가나디3', '1234', NOW(), NOW()); + +-- 4번 유저 +INSERT INTO users (user_id, email, name, nickname, password, created_at, updated_at) +VALUES (4, 'test4@test.com', '강아지4', '가나디4', '1234', NOW(), NOW()); + +-- 5번 유저 +INSERT INTO users (user_id, email, name, nickname, password, created_at, updated_at) +VALUES (5, 'test5@test.com', '강아지5', '가나디5', '1234', NOW(), NOW()); + +-- 6번 유저 +INSERT INTO users (user_id, email, name, nickname, password, created_at, updated_at) +VALUES (6, 'test6@test.com', '강아지6', '가나디6', '1234', NOW(), NOW()); \ No newline at end of file From e10645d5db43c17e6bc0fd709dd5c0aa9fbde0fa Mon Sep 17 00:00:00 2001 From: Kunhee Lee Date: Tue, 28 Apr 2026 18:15:37 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=88=A8=EA=B9=80=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PostReportController.java | 8 ++ .../exception/code/ReportErrorCode.java | 3 +- .../repository/PostReportRepository.java | 3 + .../report/service/PostReportService.java | 82 ++++++++++++++----- 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java index 8c069f5a..58ad03c7 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/controller/PostReportController.java @@ -23,4 +23,12 @@ public ApiResponse createReport( PostReportResponseDTO response = postReportService.reportPost(postId, request.reporterId(), request.reason()); return ApiResponse.onSuccess("REPORT201", "신고가 정상적으로 접수되었습니다.", response); } + + @PatchMapping("/reports/{reportId}/resolve") + public ApiResponse resolveReport( + @PathVariable Long reportId + ) { + PostReportResponseDTO response = postReportService.resolveReport(reportId); + return ApiResponse.onSuccess("REPORT200_1", "신고 처리가 완료되어 해당 게시글이 숨겨졌습니다.", response); + } } \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java index 4eeb34c4..640bf4df 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/exception/code/ReportErrorCode.java @@ -10,7 +10,8 @@ public enum ReportErrorCode implements BaseErrorCode { REPORT_DUPLICATED("REPORT409_1", "이미 신고한 게시글입니다.", HttpStatus.CONFLICT), REPORT_SELF_FORBIDDEN("REPORT403_1", "자신의 게시글은 신고할 수 없습니다.", HttpStatus.FORBIDDEN), - REPORT_NOT_FOUND("REPORT404_1", "해당 신고 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND); + REPORT_NOT_FOUND("REPORT404_1", "해당 신고 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + REPORT_ALREADY_RESOLVED("REPORT400_1", "이미 처리가 완료된 신고입니다.", HttpStatus.BAD_REQUEST); private final String code; private final String message; diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java index 814f1e45..c96922c8 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/repository/PostReportRepository.java @@ -6,6 +6,9 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface PostReportRepository extends JpaRepository { + // 특정 게시글의 전체 신고 횟수를 카운트하는 메서드 + // Post 엔티티 필드명(post) + Post 엔티티 내부의 PK 필드명(PostId) + long countByPost_PostId(Long postId); // 중복 신고 확인: 동일 신고자가 동일 게시글을 이미 신고했는지 체크 boolean existsByReporter_UserIdAndPost_PostId(Long userId, Long postId); } \ No newline at end of file diff --git a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java index fd2d6822..72147b5c 100644 --- a/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java +++ b/LeeKunHee/src/main/java/com/leets/assignment/domain/report/service/PostReportService.java @@ -1,11 +1,13 @@ package com.leets.assignment.domain.report.service; import com.leets.assignment.domain.post.entity.Post; +import com.leets.assignment.domain.post.entity.PostStatus; import com.leets.assignment.domain.post.exception.PostException; import com.leets.assignment.domain.post.exception.code.PostErrorCode; import com.leets.assignment.domain.post.repository.PostRepository; import com.leets.assignment.domain.report.dto.res.PostReportResponseDTO; import com.leets.assignment.domain.report.entity.PostReport; +import com.leets.assignment.domain.report.entity.ReportStatus; import com.leets.assignment.domain.report.exception.ReportException; import com.leets.assignment.domain.report.exception.code.ReportErrorCode; import com.leets.assignment.domain.report.repository.PostReportRepository; @@ -25,43 +27,85 @@ public class PostReportService { @Transactional public PostReportResponseDTO reportPost(Long postId, Long reporterId, String reason) { - // 1. 엔티티 조회 - // 유저가 없는 경우는 일반 런타임 예외나 별도의 UserException을 던질 수 있습니다. - User reporter = userRepository.findById(reporterId) - .orElseThrow(() -> new RuntimeException("존재하지 않는 유저입니다.")); + // 1. 검증 (중복/자기 신고 체크) + validateReport(postId, reporterId); + + // 2. 신고 저장 + PostReport report = postReportRepository.save(createReport(postId, reporterId, reason)); + + // 3. [핵심] 누적 신고 횟수 확인 (3회 이상이면 자동 숨김) + long reportCount = postReportRepository.countByPost_PostId(postId); + if (reportCount >= 3) { + Post post = report.getPost(); + if (post.getStatus() == PostStatus.ACTIVE) { + post.hideByAdmin(); // 누적 신고 임계치 도달 시 자동 숨김 + } + } + + return convertToDTO(report); + } - // 게시글이 없는 경우 PostErrorCode 사용 + // --- 내부 헬퍼 메서드 (기존 로직을 깔끔하게 정리) --- + + private void validateReport(Long postId, Long reporterId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND)); - // 2. 권한 및 중복 체크 (Report 전용 예외 사용) - // 본인 글 신고 불가 if (post.getUser().getUserId().equals(reporterId)) { throw new ReportException(ReportErrorCode.REPORT_SELF_FORBIDDEN); } - // 중복 신고 체크 if (postReportRepository.existsByReporter_UserIdAndPost_PostId(reporterId, postId)) { throw new ReportException(ReportErrorCode.REPORT_DUPLICATED); } + } + + private PostReport createReport(Long postId, Long reporterId, String reason) { + User reporter = userRepository.findById(reporterId) + .orElseThrow(() -> new RuntimeException("존재하지 않는 유저입니다.")); + Post post = postRepository.findById(postId).orElseThrow(); - // 3. 신고 엔티티 생성 및 저장 - PostReport report = PostReport.builder() + return PostReport.builder() .reporter(reporter) .post(post) .reason(reason) .build(); + } - PostReport savedReport = postReportRepository.save(report); - - // 4. DTO 변환 및 반환 + private PostReportResponseDTO convertToDTO(PostReport report) { return PostReportResponseDTO.builder() - .reportId(savedReport.getReportId()) - .postId(savedReport.getPost().getPostId()) - .reporterId(savedReport.getReporter().getUserId()) - .reason(savedReport.getReason()) - .status(savedReport.getStatus()) - .createdAt(savedReport.getCreatedAt()) + .reportId(report.getReportId()) + .postId(report.getPost().getPostId()) + .reporterId(report.getReporter().getUserId()) + .reason(report.getReason()) + .status(report.getStatus()) + .createdAt(report.getCreatedAt()) .build(); } + + @Transactional + public PostReportResponseDTO resolveReport(Long reportId) { + // 1. 신고 존재 확인 + PostReport report = postReportRepository.findById(reportId) + .orElseThrow(() -> new ReportException(ReportErrorCode.REPORT_NOT_FOUND)); + + // 2. 연결된 게시글 존재 확인 + Post post = report.getPost(); + if (post == null) { + throw new PostException(PostErrorCode.POST_NOT_FOUND); + } + + // 3. 상태 변경 (이미 RESOLVED인 경우 중복 처리 방지) + if (report.getStatus() == ReportStatus.RESOLVED) { + throw new ReportException(ReportErrorCode.REPORT_ALREADY_RESOLVED); + } + + report.resolve(); + post.hideByAdmin(); // Post 엔티티 내 status = PostStatus.HIDDEN_BY_ADMIN + + return convertToDTO(report); + + } + + } \ No newline at end of file